> ## Documentation Index
> Fetch the complete documentation index at: https://docs.callva.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Calls and outbound dispatch

> The call record, how to list, filter, aggregate and change calls, and how POST /call places an outbound call and what happens after it.

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:

```bash theme={null}
export CALLVA_API="https://api.callva.io/api/v1/external"
export CALLVA_TOKEN="<your API token>"
```

> [!NOTE]
>
> 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](https://docs.callva.io/authentication.md).

## 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](https://docs.callva.io/guides/custom-fields.md) and returned at the top level of the record, keyed by its field key. Dates and times are ISO 8601 in UTC.

```json A call record theme={null}
{
  "id": "01a1c74e-54f2-73f6-8af6-3ef541efe3a0",
  "tenant_id": "01a1c749-5387-7002-8154-5a7913629e6e",
  "project_id": "01a1c749-5525-731a-ab9f-61ffbf9e4ef2",
  "created_at": "2026-11-10T21:12:41.000000Z",
  "updated_at": "2026-11-10T21:12:41.000000Z",
  "cost": 0.42,
  "direction": "outbound",
  "duration": 125,
  "ended_at": "2026-11-09T14:02:05.000000Z",
  "name": "Alex Example",
  "phone": "+15555550100",
  "started_at": "2026-11-09T14:00:00.000000Z",
  "status": "complete"
}
```

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.

| Field key | Type | What the platform writes there |
| - | - | - |
| `direction` | select | `outbound` or `inbound` |
| `status` | select | Where the call has got to: `scheduled`, `calling`, `starting`, `in_progress`, `complete`, `failed` |
| `phone` | text | The person's number. A report from the voice provider fills it only when it is empty |
| `line` | text | Your own number the call ran on |
| `name` | text | The person's name. Required when a call is created through the API |
| `type` | select | `sip` for a phone call, `webrtc` for a web call |
| `provider` | select | Which voice provider ran the call, such as `callva` or `livekit` |
| `started_at`, `ended_at` | datetime | When the call started and ended |
| `duration` | integer | Length of the call in seconds |
| `cost` | decimal | What the call cost |
| `transcript` | transcript | A reference to the stored transcript. Read it through the transcript endpoint |
| `recording_url` | text | Where the recording is, written once the upload has really happened |

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.

> [!NOTE]
>
> `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`](https://docs.callva.io/api-reference/calls/list-calls) returns a page of calls. Every filterable field of the project filters the list by its key.

| Query | Meaning |
| - | - |
| `field_key=value` | Exact match |
| `field_key_gt`, `_gte`, `_lt`, `_lte` | Greater or less than, for numbers, dates and datetimes |
| `field_key_ne` | Not equal, for numbers, dates and datetimes |
| `field_key_like` | Pattern match, for text and select fields |
| `field_key_in`, `field_key_nin` | In or not in a comma-separated list, for text and select fields |
| `field_key_nullneq` | Not equal, or no value at all |
| `field_key_null`, `_not_null`, `_empty`, `_not_empty` | The field has, or has no, value |
| `search` | Text searched across every field marked searchable |
| `sort` | A sortable field key, `-` in front for descending; several comma-separated. `created_at`, `updated_at` and `id` always sort |
| `group` | A groupable field key to group the list by; it becomes the primary sort |
| `per_page`, `page` | Page size (default 30, at most 1000, or `all`) and page number from 1 |

Filters combine with AND. `created_at` and `updated_at` filter like datetime fields.

```bash curl theme={null}
curl -G "$CALLVA_API/calls" \
  -H "Authorization: Bearer $CALLVA_TOKEN" \
  -H "Accept: application/json" \
  --data-urlencode "status=complete" \
  --data-urlencode "duration_gte=60" \
  --data-urlencode "sort=-started_at" \
  --data-urlencode "per_page=10"
```

```python Python theme={null}
import os
import requests

api = os.environ["CALLVA_API"]
headers = {
    "Authorization": f"Bearer {os.environ['CALLVA_TOKEN']}",
    "Accept": "application/json",
}

response = requests.get(
    f"{api}/calls",
    headers=headers,
    params={
        "status": "complete",
        "duration_gte": 60,
        "sort": "-started_at",
        "per_page": 10,
    },
)
response.raise_for_status()
body = response.json()
for call in body["data"]:
    print(call["id"], call.get("status"), call.get("duration"))
print("total:", body["pagination"]["total"])
```

```javascript JavaScript theme={null}
const params = new URLSearchParams({
  status: "complete",
  duration_gte: "60",
  sort: "-started_at",
  per_page: "10",
});

const response = await fetch(`${process.env.CALLVA_API}/calls?${params}`, {
  headers: {
    Authorization: `Bearer ${process.env.CALLVA_TOKEN}`,
    Accept: "application/json",
  },
});
const { data, pagination } = await response.json();
for (const call of data) console.log(call.id, call.status, call.duration);
console.log("total:", pagination.total);
```

```json Response theme={null}
{
  "success": true,
  "message": "Calls retrieved successfully",
  "data": [
    {
      "id": "01a1c74e-54f2-73f6-8af6-3ef541efe3a0",
      "tenant_id": "01a1c749-5387-7002-8154-5a7913629e6e",
      "project_id": "01a1c749-5525-731a-ab9f-61ffbf9e4ef2",
      "created_at": "2026-11-10T21:12:41.000000Z",
      "updated_at": "2026-11-10T21:12:41.000000Z",
      "cost": 0.42,
      "direction": "outbound",
      "duration": 125,
      "ended_at": "2026-11-09T14:02:05.000000Z",
      "name": "Alex Example",
      "phone": "+15555550100",
      "started_at": "2026-11-09T14:00:00.000000Z",
      "status": "complete"
    }
  ],
  "pagination": { "current_page": 1, "last_page": 1, "per_page": 10, "total": 1, "from": 1, "to": 1 }
}
```

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`:

> [accordion] Unknown field
>
> ```json theme={null}
> {
>   "success": false,
>   "message": "Cannot filter by field 'colour': Unknown field. This field does not exist for this entity type.",
>   "errors": { "colour": ["Cannot filter by field 'colour': Unknown field. This field does not exist for this entity type."] }
> }
> ```

> [accordion] Field not marked filterable
>
> ```json theme={null}
> {
>   "success": false,
>   "message": "Cannot filter by field 'error': This field exists but filtering is not enabled (is_filterable=false). Please enable filtering for this field or contact your administrator.",
>   "errors": { "error": ["Cannot filter by field 'error': This field exists but filtering is not enabled (is_filterable=false). Please enable filtering for this field or contact your administrator."] }
> }
> ```

> [accordion] 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.
>
> ```json theme={null}
> {
>   "success": false,
>   "message": "Filter operation 'ne' is not supported for field 'status' of type 'string'. Supported operations: =, like, in, nin, nullneq, null, not_null, empty, not_empty",
>   "errors": { "query": ["Filter operation 'ne' is not supported for field 'status' of type 'string'. Supported operations: =, like, in, nin, nullneq, null, not_null, empty, not_empty"] }
> }
> ```

> [!TIP]
>
> 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}`](https://docs.callva.io/api-reference/calls/get-a-call) returns one record in the same shape, or `404` with `"Call not found"` when the id is not a call in your project.

```bash theme={null}
curl "$CALLVA_API/calls/01a1c74e-65e9-70ec-a341-1c9de3dd7513" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json"
```

### Transcript

The `transcript` field holds only a reference; the conversation itself is stored as a file. Read it with [`GET /calls/{call}/transcript`](https://docs.callva.io/api-reference/transcripts/get-a-calls-transcript):

```bash theme={null}
curl "$CALLVA_API/calls/01a1c74e-65e9-70ec-a341-1c9de3dd7513/transcript" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json"
```

```json Response theme={null}
{
  "success": true,
  "message": "Transcript retrieved successfully",
  "data": {
    "version": 1,
    "call_id": "01a1c74e-65e9-70ec-a341-1c9de3dd7513",
    "tenant_id": "01a1c749-5387-7002-8154-5a7913629e6e",
    "project_id": "01a1c749-5525-731a-ab9f-61ffbf9e4ef2",
    "created_at": "2026-11-10T21:13:26+00:00",
    "messages": [
      { "role": "assistant", "content": "Hello, this is the clinic calling about your appointment tomorrow.", "timestamp": "2026-11-10T21:20:02Z" },
      { "role": "user", "content": "Yes, I will be there.", "timestamp": "2026-11-10T21:20:06Z" }
    ],
    "metadata": { "total_messages": 2 }
  }
}
```

A call with no transcript answers `404` with `"No transcript found for this call"`. The other transcript endpoints:

* [`GET /calls/{call}/transcript/url`](https://docs.callva.io/api-reference/transcripts/get-a-transcript-download-link) returns a presigned `url` to download the file directly, valid for `expires_in` seconds (3600).
* [`POST /calls/{call}/transcript`](https://docs.callva.io/api-reference/transcripts/save-a-calls-transcript) and [`PUT`](https://docs.callva.io/api-reference/transcripts/replace-a-calls-transcript) 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`](https://docs.callva.io/api-reference/transcripts/delete-a-calls-transcript) deletes the file and clears the field.

### Recording

[`GET /calls/{call}/recording`](https://docs.callva.io/api-reference/recordings/stream-a-calls-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`](https://docs.callva.io/api-reference/recordings/purge-a-recordings-cache) 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`](https://docs.callva.io/api-reference/webhook-logs/list-a-calls-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`](https://docs.callva.io/api-reference/automations/list-a-calls-automation-runs) lists the automations that ran for it. See [Webhooks](https://docs.callva.io/guides/webhooks.md) and [Automations](https://docs.callva.io/guides/automations.md).

### Aggregates and trends

[`GET /calls/aggregate`](https://docs.callva.io/api-reference/calls/aggregate-calls) 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.

```bash theme={null}
curl -G "$CALLVA_API/calls/aggregate" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json" \
  -d operation=sum -d field=duration -d status=complete
```

```json Response theme={null}
{ "success": true, "message": "Aggregate retrieved successfully", "data": { "operation": "sum", "field": "duration", "value": 165 } }
```

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"`.

```bash theme={null}
curl -G "$CALLVA_API/calls/aggregate" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json" \
  -d operation=count -d group_by=status
```

```json Response theme={null}
{
  "success": true,
  "message": "Grouped aggregate retrieved successfully",
  "data": {
    "operation": "count",
    "field": null,
    "group_by": "status",
    "groups": [
      { "key": "complete", "value": 2 },
      { "key": "scheduled", "value": 2 },
      { "key": "null", "value": 1 }
    ]
  }
}
```

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`](https://docs.callva.io/api-reference/calls/get-call-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`.

```bash theme={null}
curl -G "$CALLVA_API/calls/trends" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json" \
  -d date_from=2026-11-08T00:00:00Z -d date_to=2026-11-10T23:59:59Z
```

```json Response theme={null}
{
  "success": true,
  "message": "Trends retrieved successfully",
  "data": {
    "data": [
      { "date": "2026-11-08T00:00:00.000000Z", "count": 0, "label": "Nov 8" },
      { "date": "2026-11-09T00:00:00.000000Z", "count": 0, "label": "Nov 9" },
      { "date": "2026-11-10T00:00:00.000000Z", "count": 4, "label": "Nov 10" }
    ],
    "date_from": "2026-11-08T00:00:00.000000Z",
    "date_to": "2026-11-10T23:59:59.000000Z",
    "total": 4
  }
}
```

## 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`](https://docs.callva.io/api-reference/calls/create-a-call) 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.

```bash theme={null}
curl -X POST "$CALLVA_API/calls" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alex Example",
    "phone": "+15555550100",
    "direction": "outbound",
    "status": "complete",
    "started_at": "2026-11-09T14:00:00Z",
    "ended_at": "2026-11-09T14:02:05Z",
    "duration": 125,
    "cost": 0.42
  }'
```

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:

```json theme={null}
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "duration": ["The duration field must be an integer."],
    "status": ["The selected status is invalid."]
  }
}
```

> [!WARNING]
>
> 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}`](https://docs.callva.io/api-reference/calls/update-a-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.

```bash theme={null}
curl -X PUT "$CALLVA_API/calls/01a1c74e-65e9-70ec-a341-1c9de3dd7513" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"status": "in_progress", "started_at": "2026-11-10T21:20:00Z"}'
```

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`](https://docs.callva.io/api-reference/calls/update-calls-in-bulk) applies one set of values to a list of calls in one transaction.

```bash theme={null}
curl -X PATCH "$CALLVA_API/calls/batch" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": ["01a1c74e-54f2-73f6-8af6-3ef541efe3a0", "01a1c74e-6512-7211-b944-187dcc2e84c5"],
    "updates": {"cost": 0.5}
  }'
```

```json Response theme={null}
{
  "success": true,
  "message": "Calls updated successfully",
  "data": {
    "updated_count": 2,
    "ids": ["01a1c74e-54f2-73f6-8af6-3ef541efe3a0", "01a1c74e-6512-7211-b944-187dcc2e84c5"]
  }
}
```

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}`](https://docs.callva.io/api-reference/calls/delete-a-call) deletes the call, its field values and its stored transcript.

```bash theme={null}
curl -X DELETE "$CALLVA_API/calls/01a1c74e-66a7-72f7-a311-7c6614ebe71c" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json"
```

```json Response theme={null}
{ "success": true, "message": "Call deleted successfully" }
```

## Placing an outbound call

[`POST /call`](https://docs.callva.io/api-reference/outbound-calls/place-an-outbound-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.

> [!WARNING]
>
> `POST /call` places a real phone call and costs money. Use your own test number while you build.

### The request

**destination** `string` *required*

The number to call, in E.164 form: `+` and up to 15 digits, for example `+15555550100`.

**agent\_id** `string` *required*

The id of an active agent in the project.

**phone\_number\_id** `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.

**call\_id** `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.

**custom\_fields** `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.

**overrides** `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.

```bash curl theme={null}
curl -X POST "$CALLVA_API/call" \
  -H "Authorization: Bearer $CALLVA_TOKEN" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "01a1c749-5593-707d-877f-f89da2be547b",
    "destination": "+15555550100",
    "custom_fields": {"name": "Alex Example"},
    "overrides": {
      "agent": {
        "greeting_type": "message",
        "greeting": "Hi, this is the clinic calling about your appointment tomorrow.",
        "max_duration_seconds": 300
      }
    }
  }'
```

```python Python theme={null}
import os
import requests

api = os.environ["CALLVA_API"]
headers = {
    "Authorization": f"Bearer {os.environ['CALLVA_TOKEN']}",
    "Accept": "application/json",
}

response = requests.post(
    f"{api}/call",
    headers=headers,
    json={
        "agent_id": "01a1c749-5593-707d-877f-f89da2be547b",
        "destination": "+15555550100",
        "custom_fields": {"name": "Alex Example"},
        "overrides": {
            "agent": {
                "greeting_type": "message",
                "greeting": "Hi, this is the clinic calling about your appointment tomorrow.",
                "max_duration_seconds": 300,
            }
        },
    },
)
body = response.json()
if response.status_code == 201:
    print("placed call", body["data"]["call"]["id"])
else:
    print("refused:", response.status_code, body["errors"].get("error"), body["message"])
```

```javascript JavaScript theme={null}
const response = await fetch(`${process.env.CALLVA_API}/call`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CALLVA_TOKEN}`,
    Accept: "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agent_id: "01a1c749-5593-707d-877f-f89da2be547b",
    destination: "+15555550100",
    custom_fields: { name: "Alex Example" },
    overrides: {
      agent: {
        greeting_type: "message",
        greeting: "Hi, this is the clinic calling about your appointment tomorrow.",
        max_duration_seconds: 300,
      },
    },
  }),
});
const body = await response.json();
if (response.status === 201) console.log("placed call", body.data.call.id);
else console.log("refused:", response.status, body.errors?.error, body.message);
```

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.

```json Response (shape) theme={null}
{
  "success": true,
  "message": "Outbound call initiated successfully",
  "data": {
    "call": {
      "id": "…",
      "created_at": "…",
      "direction": "outbound",
      "status": "calling",
      "phone": "+15555550100",
      "line": "<the number the call is placed from>",
      "type": "sip",
      "provider": "callva",
      "started_at": "…",
      "name": "Alex Example"
    },
    "room": { "name": "callva_…", "url": "wss://…", "…": "…" },
    "sip_participant": { "…": "…" },
    "config": { "agent": { "…": "…" }, "preset": { "…": "…" }, "services": { "…": "…" }, "tools": { "…": "…" }, "call": { "…": "…" } }
  }
}
```

### Two override shapes, never both

A dispatch carries overrides in one of two shapes.

> [tab] Unified (use this)
>
> 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.
>
> ```json theme={null}
> {
>   "overrides": {
>     "agent": { "greeting_type": "message", "greeting": "Hi there.", "max_duration_seconds": 300 },
>     "services": { "copilot": { "enabled": false } }
>   }
> }
> ```
>
> 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`](https://docs.callva.io/api-reference/presets/list-the-preset-catalogue) lists what each preset takes.

> [tab] Legacy
>
> A flat `agent_overrides` object with parallel `preset_overrides`, `services_overrides` and `tools_overrides`. The platform translates it into the unified shape before compiling: `max_call_duration` becomes `max_duration_seconds`, `agent_speaks_first` becomes the inverse `agent_waits_for_user`, `greeting_exact` and `greeting_instruction` become `greeting` with type `message` or `prompt`, and `null` means "do not override".
>
> ```json theme={null}
> {
>   "agent_overrides": { "greeting_exact": "Hi there.", "max_call_duration": 300 },
>   "services_overrides": { "copilot": { "enabled": false } }
> }
> ```
>
> A legacy `voice` or `temperature` is moved to the top of the preset's configuration. If the preset the call runs on keeps no such key there, the request is refused with `422` naming `agent_overrides.voice` or `agent_overrides.temperature`; set it inside `preset_overrides.config` at the key that preset declares.

A request carrying a non-empty `overrides` together with any legacy `*_overrides` key is refused with `422`. Neither shape silently wins; send one.

```json theme={null}
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "error": "validation_error",
    "overrides": ["Request mixes the unified `overrides` shape with legacy `*_overrides` keys. Use one or the other."]
  }
}
```

> [!NOTE]
>
> 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.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant You
    participant API as CallVA API
    participant LK as LiveKit
    participant W as Voice worker
    participant H as Your webhook
    You->>API: POST /call
    API->>API: Check agent, number, balance, concurrency
    API->>API: Create call record (status: scheduled)
    API->>LK: Create room with compiled config
    API->>LK: Create SIP participant (dial destination)
    API->>API: status: calling
    API-->>You: 201 call, room, sip_participant, config
    LK->>W: Worker joins the room
    W->>API: call.dialing report
    W->>API: call.started report (answered)
    API->>API: status: in_progress
    Note over W,LK: Conversation
    W->>API: call.ended report
    API->>API: status complete or failed, ended_at, duration, transcript stored
    W->>API: call.recording report
    API->>API: recording_url written
    API-->>H: Each report forwarded, if the agent has a custom webhook
```

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](https://docs.callva.io/guides/webhooks.md) and [Automations](https://docs.callva.io/guides/automations.md).

### Errors

A refused dispatch names its cause in `errors.error`, so you can branch on the code rather than parse the message.

| `errors.error` | Status | Cause |
| - | - | - |
| `validation_error` | 422 | The request is malformed: a destination not in E.164 form, a missing `agent_id`, an unknown `custom_fields` key, the two override shapes mixed, or a legacy `voice` or `temperature` the preset has no place for |
| `agent_not_found` | 404 | The agent does not exist in this project |
| `agent_disabled` | 422 | The agent is inactive |
| `phone_number_not_found` | 404 | The named number is not assigned to this project, or the agent has no active number |
| `phone_number_disabled` | 422 | The number is inactive |
| `phone_number_no_trunk` | 422 | The number has no outbound trunk configured |
| `insufficient_balance` | 402 | The wallet balance is exhausted |
| `concurrency_limit_exceeded` | 422 | The organization is at its concurrent-call limit; `errors.limit` and `errors.current_count` give the numbers |
| `concurrency_check_failed` | 503 | The number of active calls could not be read, so the check fails closed |
| `call_not_found` | 404 | The `call_id` does not exist |
| `livekit_error` | 503 | Any unexpected failure, including room and SIP creation |

A validation error carries `errors.error` beside the per-field messages:

```json theme={null}
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "error": "validation_error",
    "destination": ["The destination field format is invalid."]
  }
}
```

Every other refusal carries the same four keys. `reason_code` repeats the code, except for `livekit_error`, where it is `internal_error`:

```json theme={null}
{
  "success": false,
  "message": "Agent not found",
  "errors": {
    "error": "agent_not_found",
    "action": "terminate",
    "reason_code": "agent_not_found",
    "caller_message": null
  }
}
```

> [!TIP]
>
> 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](https://docs.callva.io/mcp.md) and [CLI](https://docs.callva.io/cli.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.