POST /call. The examples use these two variables:
What a token can do depends on the permissions of its role. Reading calls needs
calls.read; creating, updating and deleting them need calls.create, calls.update and calls.delete. Transcripts and recordings have their own permissions (transcripts.*, recordings.*), and placing a call needs outbound_calls.create. A request the role does not allow answers 403. See Authentication.The call record
A call has only five fixed properties:id, tenant_id, project_id, created_at and updated_at. Every other value is a field of the project’s data model, defined at runtime as a custom field and returned at the top level of the record, keyed by its field key. Dates and times are ISO 8601 in UTC.
A call record
null. A field can be added after calls exist, so a missing value is normal.
The fields the platform writes
Because the record has no fixed data columns, the platform writes what it knows about a call into fields with well-known keys. A project created from one of the platform’s templates carries them as system fields;GET /custom-fields lists exactly what your project has.
If the project defines a
result field, the platform also writes a first business result when a call ends: no_answer for an unanswered call, ignored for a rejected or canceled one, and unclear for a connected call, because telephony cannot tell whether the conversation succeeded. Your own extraction or automation replaces it with the real outcome.
status says where the call has got to, not how it went. An unanswered call still ends as complete; failed means the call could not be placed or the telephony leg failed.Reading calls
List and filter
GET /calls returns a page of calls. Every filterable field of the project filters the list by its key.
Filters combine with AND.
created_at and updated_at filter like datetime fields.
Response
422, never silently ignored, so a typo does not return the whole table. The offending parameter is named in errors:
Unknown field
Unknown field
Field not marked filterable
Field not marked filterable
Operator the field type does not take
Operator the field type does not take
status_ne=complete is refused because status is a select field. Use status_nin=complete, or status_nullneq=complete to include calls with no status.One call
GET /calls/{call} returns one record in the same shape, or 404 with "Call not found" when the id is not a call in your project.
Transcript
Thetranscript field holds only a reference; the conversation itself is stored as a file. Read it with GET /calls/{call}/transcript:
Response
404 with "No transcript found for this call". The other transcript endpoints:
GET /calls/{call}/transcript/urlreturns a presignedurlto download the file directly, valid forexpires_inseconds (3600).POST /calls/{call}/transcriptandPUTstore a transcript you bring yourself, for example from a human call center. The body is{"messages": [{"role", "content", "timestamp"}]}withroleone ofuser,assistantorsystem; a stored transcript is replaced. It answers201with the storage reference written to thetranscriptfield.DELETE /calls/{call}/transcriptdeletes the file and clears the field.
Recording
GET /calls/{call}/recording streams the audio, with an ETag and Cache-Control: private; send If-None-Match to get 304 when your copy is current. A call with no recording answers 404. DELETE /calls/{call}/recording purges the platform’s cached copy so the next request fetches it fresh from storage; it does not delete the recording.
What happened around a call
GET /calls/{call}/webhook-logs lists every request logged for the call, including the dispatch request, room creation and the voice provider’s reports. GET /calls/{call}/automation-runs lists the automations that ran for it. See Webhooks and Automations.
Aggregates and trends
GET /calls/aggregate runs count, sum, avg, min or max over a field. It takes the same filters as the list, so a filtered aggregate and a filtered list agree on which calls they cover.
Response
group_by with a field key to break the result down. Calls with no value for that field are grouped under the key "null".
Response
created_at and set interval to day, week or month: operation=count&group_by=created_at&interval=day answers groups such as {"key": "2026-11-10", "value": 3}. interval applies only to created_at; grouping by another datetime field groups by its exact value. field is required for every operation except count, and a field key the project does not have answers 422.
GET /calls/trends returns a daily count of calls created between date_from and date_to, one bucket per day with zeros filled in. Calls whose status is paused or blocked, or that have no status, are not counted. date_to is a point in time, so pass the end of the last day to include it. A range over 365 days answers 422.
Response
Writing calls
The body of a create or update is the call’s field values, keyed by field key. Values are validated against the project’s current field definitions: the type decides the rule, a required field must be present on create, and a select field takes only its declared option values.Create
POST /calls creates a call record. It dials nothing. Use it to queue people to call, to import calls made elsewhere, or to record the calls of a human call center.
201 with the record shown at the top of this page. A value of the wrong type, or a select value that is not one of the options, answers 422 with each field named:
transcript field cannot be written this way; sending it answers 422 with "Field 'transcript' holds a transcript; write it through the call's transcript endpoint."
Update
PUT /calls/{call} writes only the fields in the body and leaves the others as they are. A required field may be left out, but if it is sent it must not be empty.
null to clear a field; it then reads as null.
Batch update, all or nothing
PATCH /calls/batch applies one set of values to a list of calls in one transaction.
Response
updates is not a field of the project, nothing is written and the response names the problem, for example "ids.1": ["Call 01a1c74e-0000-7000-8000-000000000000 was not found in this project."] or "updates.colour": ["Field 'colour' is not defined for calls in this project."].
Delete
DELETE /calls/{call} deletes the call, its field values and its stored transcript.
Response
Placing an outbound call
POST /call places a call now. The path is singular: /calls is the record resource, /call dials. The request names an active agent of your project and a destination number. The platform compiles the agent’s configuration with any overrides you send, creates a LiveKit room carrying that configuration, and dials the destination over SIP from one of your numbers.
The request
string
required
The number to call, in E.164 form:
+ and up to 15 digits, for example +15555550100.string
required
The id of an active agent in the project.
string
The number to call from. Without it, the agent’s active number is used. The number must be assigned to the project, active, and have an outbound trunk.
string
An existing call record to place this attempt on, such as a call you queued with
POST /calls. Without it, a new record is created.object
Field values to write onto the new call record, keyed by field key. A key the project does not define is refused. They are written only when the dispatch creates the record, not when
call_id names an existing one; update that call with PUT /calls/{call} first instead.object
Per-call overrides in the unified shape, mirroring the compiled agent configuration: optional
agent, preset, services and tools subtrees. They apply on top of the agent’s own settings for this call only.201. call is the new record with its field values, room and sip_participant are the LiveKit room and SIP leg the call runs in, and config is the compiled configuration exactly as the voice worker receives it. When a call behaves unexpectedly, config is the first place to look. A token issued through MCP sees stored tool credentials masked in it.
Response (shape)
Two override shapes, never both
A dispatch carries overrides in one of two shapes.- Unified (use this)
- Legacy
One Naming a preset other than the one the agent runs replaces the whole preset block rather than editing it, so such a request carries that speech stack’s whole configuration.
overrides object that mirrors the compiled configuration. Any leaf is settable. Timeouts carry a _seconds suffix, greeting_type is message or prompt, and agent_waits_for_user says who speaks first.GET /presets lists what each preset takes.overrides together with any legacy *_overrides key is refused with 422. Neither shape silently wins; send one.
Overrides are checked for structure, not content. A key the preset does not declare still reaches the voice worker; a lenient preset drops it silently, so an override that seems to do nothing is usually a misspelled or undeclared key.
What happens next
The dispatch runs its checks in order: the request, then the agent, the phone number, the custom field keys, the wallet balance and the concurrent-call limit. Only then does it create the call record (status scheduled), create the room and dial. Once the SIP leg exists the record moves to calling and the response returns. From then on, the voice worker reports the call’s progress to the platform, which updates the record.
If room or SIP creation fails, the record is marked failed and the request answers 503 livekit_error. An unanswered, rejected or canceled call ends as complete with a result such as no_answer or ignored when the project has that field; a telephony failure ends as failed. Each step is logged and readable through the call’s webhook logs. Reports are forwarded to the agent’s custom webhook for the event types the agent subscribes to; rule-driven webhook schedules and automations can react to the call’s fields. See Webhooks and Automations.
Errors
A refused dispatch names its cause inerrors.error, so you can branch on the code rather than parse the message.
A validation error carries
errors.error beside the per-field messages:
reason_code repeats the code, except for livekit_error, where it is internal_error: