Voice IntELIEgence API
Send audio to a voice application someone already set up in the workspace, poll until it settles, and take the transcript and whatever outputs that application produces. The API calls an application an app.
Everything here needs an API key and the 15-minute pass it exchanges for:
how to get both. Paths below are relative to /external/v1/voice.
The shape of an integration
GET /apps once at start-up, then cache the name to id map
POST /apps/{id}/conversations per recording
GET /apps/{id}/conversations/{convId} poll until it settles
GET /apps/{id}/conversations/{convId}/transcript
Everything else on this page handles a particular case: progress per stage, single outputs, the audio itself, and reconciling a day's work.
Every endpoint
The Scope column is what an administrator ticks on the key. A missing scope is a 403 naming
it; an app outside the key's reach is a 404, the same as one that does not exist.
Sending and finding
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /apps | voice:read | The apps this key may use, id and name only. |
POST | /apps/{appId}/conversations | voice:write | Upload a recording and start processing. multipart/form-data. |
GET | /apps/{appId}/conversations | voice:read | An app's conversations, newest first, cursor-paged. |
GET | /apps/{appId}/conversations/{convId} | voice:read | One conversation's status. This is what you poll. |
GET | /apps/{appId}/conversations/{convId}/status | voice:read | Progress per stage. |
App ids are opaque, so call /apps once at start-up and keep the mapping in memory; it changes
only when someone edits the account. An app the key cannot reach, or one that has been retired,
is absent from the list rather than an error.
The list deliberately carries no configuration: an app's name and id are all you need to send it work. How its pipeline is assembled stays in the workspace.
Taking the result
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | .../{convId}/transcript | voice:read | Timed segments, each with its speaker and text. |
GET | .../{convId}/outputs | voice:read | Every output the app produced, keyed by kind. |
GET | .../{convId}/outputs/{kind} | voice:read | One named output. |
GET | .../{convId}/media-url | voice:read | A link to the original audio, valid for 15 minutes. |
The transcript has no single block of text: join the segments' text in order if you need one.
canonical says whether the segments are the raw or the redacted version.
Which output kinds exist is part of the app's configuration (a summary, extracted fields, a classification, whatever it was set up to produce), so read the map rather than assuming a fixed set. While the pipeline is still running the map is empty.
The media link comes back in the response body, never as a 302, so your HTTP client never
has to follow a redirect or risk forwarding your Authorization header to storage. A conversation
with no stored audio answers with an empty url.
Uploading
curl -X POST "$BASE/external/v1/voice/apps/$APP/conversations" \
-H "Authorization: Bearer $PASS" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@interview.wav"
The response is a 202 with conversation_id, status (QUEUED) and status_url, which points
at the conversation itself. Processing continues in the background, so poll status_url rather
than holding the connection open.
Always send Idempotency-Key. With it, a retry after a timeout returns the original result
instead of creating a second conversation, and a timeout is exactly the case you cannot tell apart
from success.
A file that isn't audio is refused with 422 schema_invalid before anything is stored or
charged. Elie judges that from the file's declared type, or its extension when the type is
generic. The API takes recordings up to 25 MB. A larger file is refused, and the connection
may be closed rather than answered with an error, so check the size before you send.
What "done" looks like
Poll /apps/{appId}/conversations/{convId} every 2 seconds for the first minute, then every 10
seconds. Short recordings settle in under a minute; long ones take several.
status | Meaning |
|---|---|
QUEUED · PROCESSING | Still moving. Keep polling. |
PAUSED | A stage stalled. Someone has to select Resume in the workspace; there is no API call for it. |
COMPLETE | Finished. If review_status reads WAITING_FOR_APPROVAL, a person still has to approve it in the workspace. |
PARTIAL | Finished, but a later step such as knowledge base embedding or export failed. The transcript and outputs are there. |
FAILED | Processing failed. |
When something fails, failed_stage and error_code say which step and why. Both are absent
on a healthy conversation, so branch on whether they are present rather than comparing them to
null. If you show progress to your own users, .../status breaks the run down per stage, which
tells "stuck on transcription" apart from "stuck on a later step".
Asking for a transcript before that stage finishes returns 404. Here that means not yet, not
never. Poll the conversation first, and fetch the transcript once it settles.
Billing
Charged per minute of audio, rounded up and measured once the recording is transcribed rather
than estimated up front. A workspace without credit is refused at the start with 402. A
conversation that ends FAILED is not charged.
What is not here, and will not be
No endpoint creates or edits an app, changes its pipeline, enrols a speaker, edits a transcript or a persona, corrects or approves an output, resumes or re-runs a conversation, or deletes one. No scope unlocks any of it: that is workspace configuration and human review, and both stay where a person can see what they are changing.
Full schemas
Field-level request and response shapes are in the workspace: open Developers, then API explorer. The API reference there is generated from the running service, so it cannot drift. This page is the capability reference: what exists, what each call needs, and where the surprises are.