Skip to main content
A call is one record in a project’s call queue: a person to reach, the attempts made to reach them, and how it went. Everything else in CallVA hangs off it. Agents dial it, transcripts and recordings attach to it, webhooks and automations fire on it, and billing accounts for it. This guide covers the record itself, reading and writing calls through the External API, and placing an outbound call with 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
A field that has never been written is absent from the record; one that was cleared reads as 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
A filter the project cannot answer is refused with 422, never silently ignored, so a typo does not return the whole table. The offending parameter is named in errors:
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.
Filter a select field on its stored value, never its label. status=complete matches; status=Complete does not. GET /custom-fields lists each option’s value and label.

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

The transcript field holds only a reference; the conversation itself is stored as a file. Read it with GET /calls/{call}/transcript:
Response
A call with no transcript answers 404 with "No transcript found for this call". The other transcript endpoints:
  • GET /calls/{call}/transcript/url returns a presigned url to download the file directly, valid for expires_in seconds (3600).
  • POST /calls/{call}/transcript and PUT store a transcript you bring yourself, for example from a human call center. The body is {"messages": [{"role", "content", "timestamp"}]} with role one of user, assistant or system; a stored transcript is replaced. It answers 201 with the storage reference written to the transcript field.
  • DELETE /calls/{call}/transcript deletes 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. 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
Add 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
To bucket by time, group by 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.
It answers 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:
On create and update, a key the project does not define is ignored: the call is saved without it and the response leaves it out. Compare the response with what you sent, or check keys against GET /custom-fields first. Batch update refuses an unknown key instead.
The 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.
The response is the whole updated record. Send 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
If any id is not a call in your project, or any key in 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.
POST /call places a real phone call and costs money. Use your own test number while you build.

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.
A placed call answers 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.
One 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.
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. GET /presets lists what each preset takes.
A request carrying a non-empty 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 in errors.error, so you can branch on the code rather than parse the message. A validation error carries errors.error beside the per-field messages:
Every other refusal carries the same four keys. reason_code repeats the code, except for livekit_error, where it is internal_error:
The same operations are available as MCP tools (calls_list, calls_get, calls_create, calls_update, calls_batch, call_initiate) and as CLI commands (callva calls list -f status=complete, callva call <destination> --agent-id <id>). See MCP and CLI.