Ingest API
Send call recordings into a channel: multipart upload, download from a URL, or a pre-signed upload URL. Metadata fields, responses, idempotency and errors.
The ingest API creates a conversation in a channel from an audio recording. Authenticate every request with a channel ingest token whose channel matches the channelId in the URL.
Three variants exist. All accept the same metadata and return a conversation id.
| Variant | Use when |
|---|---|
| Multipart upload | Your system holds the file and can send it in one request. |
| From URL | The recording is already at an https URL VoiceLens can fetch. |
| Pre-signed upload | Files are large, or the client should upload straight to storage. |
Accepted formats: mp3, wav, m4a, ogg/oga, webm, flac; content types include audio/mpeg, audio/wav, audio/mp4, audio/ogg, audio/webm and audio/flac. Maximum size is 500 MB. There is no duration limit.
Metadata
Every variant accepts an optional metadata object. All fields are optional.
| Field | Type | Meaning |
|---|---|---|
externalCallId | string | Your carrier or CRM call id. Unique per channel; used for deduplication and returned in webhooks. |
agentId | string (≤ 200) | Stable identifier of the agent or bot version. Drives per-agent analytics. |
agentName | string | Display name for the agent. |
agentType | "ai" or "human" | Lets you compare the two populations. Case-insensitive. |
callerNumber | string | The caller's number. |
calledNumber | string | The number called. |
calledAtUtc | ISO 8601 | When the call started. Defaults to receipt time. |
durationHintSec | number | Lets the channel's minimum-duration and sampling rules run before transcription. |
custom | object | Free-form JSON, up to 50 keys of 100 characters, 32 KB total. Returned untouched in the API and webhooks. |
The multipart variant also accepts title, externalCallId, agentId, agentName and agentType as plain form fields; they override the same keys inside metadata.
Language
Every variant accepts an optional top-level language (a form field on the multipart variant, a JSON property on the others): a BCP-47 tag such as en, sl or pt-BR. It sits outside metadata because it steers how the recording is transcribed rather than describing the call.
Precedence is language on the request, then the channel's Language setting, then automatic detection. The tag is read again on every re-transcription, so fixing a channel's language and re-transcribing corrects a batch.
An invalid tag returns 400. A tag we ship no fault lexicon for is transcribed normally but graded with the English word-based detectors; see Fault codes.
Multipart upload
/api/ingest/{channelId}/audiomultipart/form-data. Text fields must come before the file part. The file may use any field name as long as it has a filename.
curl -X POST "https://voicelens.app/api/ingest/$CHANNEL_ID/audio" \
-H "X-Ingest-Token: $VK_TOKEN" \
-F "title=Inbound enquiry 14:02" \
-F "externalCallId=CA8f2a91" \
-F "agentId=bot-v12" \
-F "agentType=ai" \
-F "language=sl" \
-F 'metadata={"callerNumber":"+4930123456","custom":{"campaign":"spring"}}' \
-F "file=@call.mp3;type=audio/mpeg"Response 201 Created
{
"conversationId": "3d2f0c1e-7a4b-4f1d-9b2e-5c6a7d8e9f00",
"status": "Pending",
"container": "vl-audio-…",
"blobName": "…/call.mp3",
"sizeBytes": 2418833,
"reused": false
}The Location header points at /api/conversations/{conversationId}. reused: true means an identical request was seen before and the existing conversation was returned.
From URL
/api/ingest/{channelId}/audio/from-urlJSON body. VoiceLens downloads the file server-side.
{
"url": "https://recordings.example.com/calls/8f2a91.mp3",
"filename": "8f2a91.mp3",
"contentType": "audio/mpeg",
"title": "Inbound enquiry 14:02",
"externalCallId": "CA8f2a91",
"language": "sl",
"metadata": { "agentId": "bot-v12", "agentType": "ai" }
}| Field | Required | Notes |
|---|---|---|
url | Yes | https only, no credentials in the URL, up to 2048 characters. Private and link-local addresses are refused. |
filename, contentType | No | Used when the origin does not provide them. |
title, externalCallId, language, metadata | No | As above. |
The download follows up to 3 redirects, times out after 5 minutes and is capped at 500 MB. Idempotency is checked before the download, so replays are cheap. Failures at the origin return 502; a URL that fails validation returns 400. The response is the same IngestUploadResponse as the multipart variant.
Pre-signed upload
For large files, or when the client should not proxy bytes through your servers, ask VoiceLens for an upload URL, PUT the file to it, then confirm.
Request an upload URL
POST/api/ingest/{channelId}/upload-urlBody
{ "filename", "contentType", "title"?, "externalCallId"?, "language"?, "metadata"? }.{ "conversationId": "3d2f0c1e-…", "status": "PendingUpload", "container": "vl-audio-…", "blobName": "…/8f2a91.mp3", "uploadUrl": "https://….blob.core.windows.net/…?sv=…&sig=…", "expiresAtUtc": "2026-09-16T10:30:00Z" }The URL is an Azure Blob SAS with write and create permission, valid for 30 minutes. An idempotent replay returns the existing conversation with
uploadUrl: "about:blank".Upload the bytes
curl -X PUT "$UPLOAD_URL" \ -H "x-ms-blob-type: BlockBlob" \ -H "Content-Type: audio/mpeg" \ --data-binary @call.mp3Confirm
POST/api/ingest/{channelId}/{conversationId}/uploadedEmpty body is fine. VoiceLens checks that the blob exists and queues the call. Returns
{ "conversationId", "status": "Pending" }, or"AlreadyProcessing"on a repeat,404if the conversation is unknown,400if the blob is missing.
Idempotency
Duplicates are collapsed on three keys, in this order:
- The
Idempotency-Keyrequest header (trimmed, up to 200 characters), scoped to the channel. - The
(channelId, externalCallId)pair whenexternalCallIdis provided. - For Twilio callbacks, the
RecordingSid.
A duplicate returns the original conversation with reused: true and does not consume minutes.
Twilio recording status callback
/api/ingest/{channelId}/twilio/recording-statusNo ingest token; authenticated by Twilio's X-Twilio-Signature against the channel's auth token. Configure this URL as recordingStatusCallback for the completed event. Returns 200 { "conversationId", "reused" }, 204 for ignored events, 403 on a bad signature or account mismatch, 409 if the channel is not configured for Twilio, 502 if the recording could not be downloaded.
See Channels and ingest tokens for the setup.
Statuses
A conversation moves through PendingUpload (pre-signed flow only), Pending, Transcribing, Analyzing, then Completed, Failed or Skipped. Poll GET /api/conversations/{id} with an API key, or, better, subscribe to conversation.completed and conversation.failed webhooks.
Errors
| Status | Cause |
|---|---|
400 | Unsupported format, missing file, invalid URL, malformed metadata, invalid language tag, text fields after the file part. |
401 | Missing or invalid token, expired token, IP not in the allowlist, token bound to a different channel. |
402 | Monthly minute allowance exhausted (usage_limit_reached). |
400 or 413 | File larger than 500 MB. |
502 | Remote download failed (from-URL, Twilio). |