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.

VariantUse when
Multipart uploadYour system holds the file and can send it in one request.
From URLThe recording is already at an https URL VoiceLens can fetch.
Pre-signed uploadFiles 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.

FieldTypeMeaning
externalCallIdstringYour carrier or CRM call id. Unique per channel; used for deduplication and returned in webhooks.
agentIdstring (≤ 200)Stable identifier of the agent or bot version. Drives per-agent analytics.
agentNamestringDisplay name for the agent.
agentType"ai" or "human"Lets you compare the two populations. Case-insensitive.
callerNumberstringThe caller's number.
calledNumberstringThe number called.
calledAtUtcISO 8601When the call started. Defaults to receipt time.
durationHintSecnumberLets the channel's minimum-duration and sampling rules run before transcription.
customobjectFree-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

POST/api/ingest/{channelId}/audio

multipart/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

POST/api/ingest/{channelId}/audio/from-url

JSON 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" }
}
FieldRequiredNotes
urlYeshttps only, no credentials in the URL, up to 2048 characters. Private and link-local addresses are refused.
filename, contentTypeNoUsed when the origin does not provide them.
title, externalCallId, language, metadataNoAs 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.

  1. Request an upload URL

    POST/api/ingest/{channelId}/upload-url

    Body { "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".

  2. Upload the bytes

    curl -X PUT "$UPLOAD_URL" \
      -H "x-ms-blob-type: BlockBlob" \
      -H "Content-Type: audio/mpeg" \
      --data-binary @call.mp3
  3. Confirm

    POST/api/ingest/{channelId}/{conversationId}/uploaded

    Empty body is fine. VoiceLens checks that the blob exists and queues the call. Returns { "conversationId", "status": "Pending" }, or "AlreadyProcessing" on a repeat, 404 if the conversation is unknown, 400 if the blob is missing.

Idempotency

Duplicates are collapsed on three keys, in this order:

  1. The Idempotency-Key request header (trimmed, up to 200 characters), scoped to the channel.
  2. The (channelId, externalCallId) pair when externalCallId is provided.
  3. For Twilio callbacks, the RecordingSid.

A duplicate returns the original conversation with reused: true and does not consume minutes.

Twilio recording status callback

POST/api/ingest/{channelId}/twilio/recording-status

No 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

StatusCause
400Unsupported format, missing file, invalid URL, malformed metadata, invalid language tag, text fields after the file part.
401Missing or invalid token, expired token, IP not in the allowlist, token bound to a different channel.
402Monthly minute allowance exhausted (usage_limit_reached).
400 or 413File larger than 500 MB.
502Remote download failed (from-URL, Twilio).