# Calls

Place outbound calls, list and fetch calls, and control live calls with actions. Paths are relative to
`https://api.contactivity.io/v1/projects/{project_id}`; auth, errors, paging and idempotency are in the
[overview](/guides/overview.md).

| Method | Path | Scope | Purpose |
|---|---|---|---|
| POST | `/calls` | `calls:write` | Place a call |
| GET | `/calls` | `calls:read` | List calls (paged) |
| GET | `/calls/{call_id}` | `calls:read` | Fetch a call |
| DELETE | `/calls/{call_id}` | `calls:write` | Archive a finished call's record |
| POST | `/calls/{call_id}/actions/{action}` | `calls:write` | Control a live call |

Recording starts and stops through actions (`record_start`, ...); the files are under [recordings](/guides/recordings.md).

## The call object

| Field | Type | Description |
|---|---|---|
| `id` | string | `CA…` |
| `parent_call_id` | string or null | The call that dialed this one (`<Dial>`, transfer) |
| `app_id` | string or null | The voice app handling the call |
| `app_kind` | string or null | Kind of that app: `coxml`, `call_control`, `ai_agent`, `flow` |
| `ai_agent` | boolean | An AI agent ran on the call (an `ai_agent` app, or `<Connect><Agent>` from another app) |
| `mode` | string | `coxml` (driven by CoXML documents) or `call_control` (driven by actions) |
| `direction` | string | `inbound` or `outbound` |
| `from`, `to` | string | E.164; a carrier's `1NXXNXXXXXX` (no `+`) is shown as `+1NXXNXXXXXX` |
| `status` | string | `queued`, `ringing`, `in_progress`, `completed`, `busy`, `failed`, `no_answer`, `canceled` |
| `answered_by` | string or null | Machine detection result: `human`, `machine_start`, `machine_end_beep`, `fax`, `unknown` |
| `created_at`, `started_at`, `answered_at`, `ended_at` | timestamp or null | |
| `duration_secs` | integer or null | |
| `price` | money or null | Set once the call is rated |
| `quality` | object or null | Media quality measured at hangup |
| `client_state` | string or null | Echoes `client_state` in the create response; null on other reads |

## Place a call

`POST /calls` returns `201` and the call (status `queued`). Give exactly one source of call logic:

| Source | Behaviour |
|---|---|
| `app_id` | A voice app of the project: a `coxml` app with a voice URL (its URL, method, fallback and status callback apply), a `call_control` app (you drive the call with actions; events go to the app's `webhook_url`, or the `webhook_url` given here), or an `ai_agent` app (the agent talks) |
| `coxml` | An inline CoXML document, at most 4000 characters |
| `coxml_url` | CoXML fetched when the call is answered (`coxml_method` `GET` or `POST`, default `POST`) |
| `webhook_url` (alone) | A call-control call without an app; events go to this URL |

| Field | Type | Rules |
|---|---|---|
| `to` | string | Required. E.164, `client:<name>`, or a SIP URI with a public host |
| `from` | string | Required. E.164; a number of the project or a verified number |
| `app_id`, `coxml`, `coxml_url`, `coxml_method`, `webhook_url` | | See above. `webhook_url` with `app_id` only for `call_control` apps |
| `status_callback_url` | string | Public https:// URL for status callbacks |
| `status_callback_events` | string[] | `initiated`, `ringing`, `answered`, `completed` (default `["completed"]`) |
| `timeout_secs` | integer | Ring time, 5-600, default 60 |
| `time_limit_secs` | integer | 1-14400, default 14400 |
| `machine_detection` | string | `disabled` (default), `detect`, `detect_words`, `detect_beep`, `greeting_end`, `premium` |
| `machine_detection_config` | object | `total_analysis_time_millis`, `after_greeting_silence_millis`, `greeting_duration_millis`, `initial_silence_millis` (each 100-60000) |
| `record` | false, `"single"` or `"dual"` | Record the call, mono or one channel per party |
| `client_state` | string | Base64, at most 4096 characters; echoed on events |

```sh
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/calls \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"to": "+14155550100", "from": "+12125550123",
       "coxml": "<Response><Say>Your order has shipped.</Say></Response>",
       "status_callback_url": "https://example.com/status", "status_callback_events": ["answered", "completed"]}'
```

Errors: `400 invalid_parameter` (param `to`, `from`, `app_id`, `coxml`, `coxml_url`, `coxml_method`,
`webhook_url`, `status_callback_url`, `status_callback_events`, `timeout_secs`, `time_limit_secs`,
`machine_detection`, `record`, `client_state`), `400 unknown_parameter`, `402 insufficient_balance`,
`403 caller_id_not_verified` (param `from`), `403 destination_blocked` (param `to`), `403 account_inactive`,
`409 app_disabled` (param `app_id`: the voice app is disabled), `429 rate_limit` (calls per second or simultaneous calls), `503 engine_unavailable`.

## List and fetch

`GET /calls` filters: `status`, `direction` (`inbound`/`outbound`), `from`, `to` (`+1NXXNXXXXXX` also matches a
stored `1NXXNXXXXXX`), `parent_call_id`, `app_id` (exact matches), `app_kind` (`coxml`, `call_control`, `ai_agent`,
`flow`; `ai_agent` also matches agents started from another app), `created_after` (inclusive) and `created_before`
(exclusive) as RFC 3339, plus `limit` and `starting_after`.

```sh
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/calls?status=completed&limit=20" \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY"
```

`GET /calls/{call_id}`: `404 not_found` when the call is not in the project.

`GET /calls/{call_id}/detail` (scope `calls:read`) returns everything stored about the call in one response:

| Field | Description |
|---|---|
| `call` | The call object plus `hangup_cause` (from the carrier CDR, or null) |
| `legs` | Child calls (`<Dial>`, transfers), each a call object with its `quality` |
| `carrier_legs` | Rated carrier CDRs: `sip_call_id`, `started_at`, `answered_at`, `duration_secs`, `price`, `hangup_cause` |
| `routing` | `number` (dialed number inbound, caller ID outbound), `route` (`id`, `type`, `target`, `target_name`: trunk, PBX app or voice app name), `route_at_call_time` (false: the current route; routes are not stored per call), `app` (`id`, `name`, `kind`, `param_style`) |
| `ai_agent` | The agent session as `GET /ai/calls/{call_id}` plus `agent_name`, `voice`, `session_sid` and `parts` (charge per part); null when no agent ran |
| `requests` | Document fetches and webhooks of the call (as the call inspector); CoXML fetches carry `verbs`, the verbs of the returned document (`Gather>Say` for nested ones) |
| `commands` | API requests that changed the call (call actions, updates) from the access audit log: `created_at`, `method`, `path`, `status`, `actor_type`, `command` |
| `events` | Native events of the call (kept 7 days), oldest first |
| `errors` | Call errors (`category`, `code`, `message`, `url`) |
| `recordings`, `transcriptions` | Of the call; archived ones are left out |
| `pricing` | `call`, `ai`, `ai_billed_secs`, `features` (`amd`, `noise_suppression` with `amount`), `transcriptions`, `total` (money) |

An API key gets `ai_agent` only with `apps:read`, and `recordings`/`transcriptions` only with `recordings:read`.
Every read is written to the access audit log as sensitive (it can carry transcripts).

`DELETE /calls/{call_id}` archives a finished call's record: it leaves `GET /calls` (list archived calls with
`?archived=true`) and is still returned by id with `archived_at` set. Its webhook deliveries, errors, events,
recordings and transcriptions are kept. `204`; `404 not_found`; `409 archived` (already archived);
`422 call_state` while the call is live.

## Actions

`POST /calls/{call_id}/actions/{action}` returns `200` and the call. `redirect` and `hangup` work on every call.
The other actions need a call-control call (placed with a `call_control` app or a `webhook_url`); on a CoXML call
they return `409 call_state` (use `redirect` to move a CoXML call to new CoXML).

Every action takes `client_state` (base64, at most 4096 characters). Other fields per action; anything else is
`400 unknown_parameter`:

| Action | Fields |
|---|---|
| `answer` | none |
| `hangup` | none |
| `reject` | `cause` |
| `redirect` | exactly one of `coxml_url` (+ `coxml_method`) or `coxml` (at most 4000 characters) |
| `bridge` | `call_id` (the other call), `park_after_unbridge` |
| `transfer` | `to`, `from`, `timeout_secs`, `machine_detection`, `machine_detection_config` |
| `refer` | `sip_address` |
| `speak` | `payload`, `payload_type`, `voice`, `language` |
| `play` / `stop_play` | `audio_url`, `loop`, `overlay` / `stop` |
| `gather` | `minimum_digits`, `maximum_digits`, `timeout_millis`, `terminating_digit`, `valid_digits`, `inter_digit_timeout_millis` |
| `gather_using_speak` | `payload`, `payload_type`, `voice`, `language`, `input`, `hints` + the `gather` fields |
| `gather_using_audio` | `audio_url` + the `gather` fields |
| `gather_using_ai` | `parameters`, `greeting`, `voice`, `language`, `message_history`, `assistant`, `send_partial_results`, `timeout_secs` |
| `stop_gather` | none |
| `record_start` | `format`, `channels`, `play_beep`, `max_length`, `transcription`, `transcription_engine`, `transcription_language` |
| `record_stop`, `record_pause`, `record_resume` | none |
| `send_dtmf` | `digits`, `duration_millis` |
| `enqueue` / `leave_queue` | `queue_name`, `max_wait_time_secs`, `max_size` / none |
| `stream_start` / `stream_stop` | `stream_url` (ws/wss), `stream_track`, `stream_bidirectional_mode` / none |
| `transcription_start` / `transcription_stop` | `language`, `transcription_engine`, `transcription_tracks`, `interim_results` / none |
| `ai_agent_start` | `voice_app_id` (an AI agent app), `parameters` |
| `noise_suppression_start` / `noise_suppression_stop` | `direction` / none |
| `update_client_state` | `client_state` (required) |

```sh
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/calls/$CALL_ID/actions/gather_using_speak \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"payload": "Press 1 for sales, 2 for support.", "maximum_digits": 1}'

curl https://api.contactivity.io/v1/projects/$PROJECT_ID/calls/$CALL_ID/actions/redirect \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"coxml_url": "https://example.com/after-hours.xml", "coxml_method": "GET"}'
```

The `Idempotency-Key` replaces Call Control's `command_id`: a retried action with the same key runs once.

Errors: `404 unknown_action` (param `action`), `404 not_found`, `400 unknown_parameter`, `400 invalid_parameter`
(param `client_state`, `coxml_url`, `coxml`, `coxml_method`, or a value the voice engine refused),
`409 call_state` (the action needs a call-control call), `422 call_state` (the call has ended or is not in a
state that allows the action), `502 api_error`, `503 engine_unavailable`.

Events of call-control calls go to the app's `webhook_url`; see [events.md](/guides/events.md).
