> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Calls

A **call** is one phone conversation between one of your agents and one phone number. Jelliu creates a call record for every outbound call it places (from `POST /api/calls`, a campaign or the MCP server) and for every inbound call that reaches one of your numbers. The record follows the call from the moment it is queued until it ends, and then collects what happened on it: status, outcome, duration, summary, transcript, structured analysis and cost telemetry.

This page covers placing a call, the checks every outbound call must pass, the status lifecycle, outcomes, durations and time limits, recordings, live transfers, hanging up, and how inbound calls are routed.

## How it works

Every outbound call, whether you place it directly or a campaign dials it, goes through the same sequence of checks before the phone rings. The first check that fails stops the call and returns an error; no call record is created for checks that run before the record is written.

```mermaid
flowchart TD
    A["POST /api/calls"] --> B{"Plan allows calls?"}
    B -- no --> X1["403 BILLING_ERROR"]
    B -- yes --> C{"Body valid?"}
    C -- no --> X2["400 VALIDATION_FAILED"]
    C -- yes --> D{"Agent paused?"}
    D -- yes --> X3["409 AGENT_PAUSED"]
    D -- no --> E["Resolve destination and contact"]
    E --> F{"Compliance: hours, opt-out, DNC, attempts"}
    F -- blocked --> X4["403 COMPLIANCE_BLOCKED"]
    F -- ok --> G{"Destination country allowed?"}
    G -- no --> X4
    G -- yes --> H{"Spam and trial-abuse checks"}
    H -- blocked --> X5["429 CALL_SPAM_DETECTED / 403 TRIAL_ABUSE_DETECTED"]
    H -- ok --> I{"Agent provisioned? Number available?"}
    I -- no --> X6["409 AGENT_PROVISIONING / 403 PHONE_NUMBER_REQUIRED"]
    I -- yes --> J{"Concurrency slot free?"}
    J -- no --> X7["429 MAX_CONCURRENT_CALLS_REACHED"]
    J -- yes --> K["Call record created: queued"]
    K --> L["Voice provider dials"]
    L -- accepted --> M["201 Created, status ringing"]
    L -- rejected --> N["Record marked failed, error returned"]
```

After the provider accepts the call, the rest of the lifecycle is asynchronous. When the conversation ends, the voice provider reports the transcript and analysis to Jelliu, which closes the record, computes the outcome and sends your [webhooks](/webhooks). A background reconciler runs every minute and closes any call whose end was not reported, so a record never stays open forever.

The response to `POST /api/calls` means the provider **accepted** the call, not that anyone answered. Track the result with `GET /api/calls/{callId}` or, better, subscribe to `call.completed` and `call.failed`.

## The call object

`GET /api/calls` and `GET /api/calls/{callId}` return the same shape. The list omits the heavier fields marked **detail only**. `POST /api/calls` returns the raw record right after dialing, without the computed fields `analysis`, `has_recording`, `agent_name`, `campaign_name` and `contact_name`.

Internal provider identifiers (the carrier call SID and the voice provider's conversation id) are never returned.

### Identity and routing

| Field               | Type          | Nullable | Description                                                                                 |
| ------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------- |
| `id`                | string (uuid) | No       | Call id.                                                                                    |
| `tenant_id`         | string (uuid) | No       | Your workspace id.                                                                          |
| `agent_id`          | string (uuid) | Yes      | The agent that handled the call. Null on very old records.                                  |
| `agent_name`        | string        | Yes      | The agent's name. Not included in the `POST` response.                                      |
| `campaign_id`       | string (uuid) | Yes      | The campaign the call belongs to. Null for standalone and inbound calls.                    |
| `campaign_name`     | string        | Yes      | The campaign's name. Not included in the `POST` response.                                   |
| `contact_id`        | string (uuid) | Yes      | The contact the call is filed under. See [Contacts](/resources/contacts).                   |
| `contact_name`      | string        | Yes      | The contact's name. Not included in the `POST` response.                                    |
| `phone_number`      | string        | No       | The other party in E.164: the number dialed on outbound calls, the caller on inbound calls. |
| `call_direction`    | string        | No       | `outbound` or `inbound`.                                                                    |
| `prompt_variant_id` | string (uuid) | Yes      | The A/B prompt variant used, if any.                                                        |

### Lifecycle

| Field                | Type               | Nullable | Description                                                                                                                                                                    |
| -------------------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status`             | string             | No       | One of `queued`, `ringing`, `in-progress`, `completed`, `failed`, `no-answer`, `busy`, `canceled`. See [Status lifecycle](#status-lifecycle).                                  |
| `outcome`            | string             | Yes      | What the call achieved, for example `appointment_booked`. Null until analysis is available. See [Outcomes](#outcomes).                                                         |
| `duration_seconds`   | integer            | Yes      | Length of the conversation in whole seconds. Null until reported.                                                                                                              |
| `started_at`         | string (date-time) | Yes      | When the conversation started.                                                                                                                                                 |
| `ended_at`           | string (date-time) | Yes      | When the record was closed.                                                                                                                                                    |
| `termination_reason` | string             | Yes      | Why the call ended, as reported by the voice provider (free text). For calls that never connected it starts with `initiation_failure:`, for example `initiation_failure:busy`. |
| `has_recording`      | boolean            | No       | `true` when audio can be requested from [`/audio`](#download-the-recording). Not included in the `POST` response.                                                              |
| `created_at`         | string (date-time) | No       | When the record was created. Lists are ordered by this field, newest first.                                                                                                    |
| `updated_at`         | string (date-time) | No       | Last change to the record.                                                                                                                                                     |
| `deleted_at`         | string (date-time) | Yes      | Always null in API responses: deleted calls are not returned.                                                                                                                  |

### Analysis

| Field                         | Type      | Nullable | Description                                                                                                                                                                                                                              |
| ----------------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `summary`                     | string    | Yes      | Short summary of the conversation. Often arrives a little after the call ends.                                                                                                                                                           |
| `sentiment_score`             | number    | Yes      | Average caller sentiment from `-1` to `1`. Null means not measured, which is different from `0`.                                                                                                                                         |
| `sentiment_low`               | number    | Yes      | The most negative caller turn, `-1` to `1`.                                                                                                                                                                                              |
| `frustration_score`           | number    | Yes      | `0` to `1`. Null means not measured; `0` means nobody was frustrated.                                                                                                                                                                    |
| `call_success_score`          | number    | Yes      | The provider's `0` to `100` confidence that the call met its goal.                                                                                                                                                                       |
| `objections`                  | string\[] | Yes      | Objections raised by the other party.                                                                                                                                                                                                    |
| `evaluation_criteria_results` | object    | No       | Map of criterion id to `{ result, rationale }`, where `result` is `success`, `failure` or `unknown`. `{}` when none.                                                                                                                     |
| `data_collection_results`     | object    | No       | Map of field id to `{ value, rationale }` with the data the agent collected. `{}` when none.                                                                                                                                             |
| `analysis`                    | object    | Yes      | Convenience view built from the fields above. Null when the call produced no analysis signal at all. Not included in the `POST` response.                                                                                                |
| `analysis.summary`            | string    | No       | `summary`, or an empty string.                                                                                                                                                                                                           |
| `analysis.outcome`            | string    | Yes      | Same as `outcome`.                                                                                                                                                                                                                       |
| `analysis.sentiment`          | string    | Yes      | `positive` (score above `0.2`), `negative` (below `-0.2`), `neutral`, or null when there is no score.                                                                                                                                    |
| `analysis.sentiment_score`    | number    | Yes      | Same as `sentiment_score`.                                                                                                                                                                                                               |
| `analysis.key_topics`         | string\[] | No       | Always an empty array today.                                                                                                                                                                                                             |
| `analysis.qualification`      | object    | Yes      | Lead qualification derived from collected data: `score` (`0` to `100` or null), `interest` (`high`, `medium`, `low` or null), `answered` (boolean or null), `next_step`, `objections`, `reasons`. Null when there is nothing to qualify. |
| `kpi_data`                    | object    | Yes      | **Detail only.** KPI values computed for the call's campaign category.                                                                                                                                                                   |
| `transcript`                  | array     | Yes      | **Detail only.** Ordered turns. See [Transcripts](#transcripts).                                                                                                                                                                         |
| `sentiment_timeline`          | array     | Yes      | **Detail only.** Per-turn sentiment as `{ t, s, f }` items.                                                                                                                                                                              |

### Telemetry

All telemetry fields are null when the provider did not report them.

| Field                                                | Type               | Nullable | Description                                                                                          |
| ---------------------------------------------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------- |
| `main_language`                                      | string             | Yes      | Language detected on the call, for example `es`.                                                     |
| `initiation_source`                                  | string             | Yes      | How the conversation was started, as reported by the provider (for example `twilio` or `sip_trunk`). |
| `agent_talk_seconds`, `user_talk_seconds`            | number             | Yes      | Seconds of agent and caller audio.                                                                   |
| `agent_turn_count`, `user_turn_count`                | integer            | Yes      | Number of turns per side.                                                                            |
| `interruption_count`                                 | integer            | Yes      | Agent turns the caller cut off.                                                                      |
| `avg_response_latency_ms`, `max_response_latency_ms` | integer            | Yes      | Agent response latency, average and worst turn.                                                      |
| `cost_credits`                                       | number             | Yes      | Voice provider credits charged for the conversation.                                                 |
| `cost_usd`, `llm_cost_usd`                           | string             | Yes      | Cost in USD as a decimal **string**, for example `"0.0412"`.                                         |
| `llm_input_tokens`, `llm_output_tokens`              | integer            | Yes      | **Detail only.** Model tokens used.                                                                  |
| `tool_call_count`                                    | integer            | Yes      | **Detail only.** Tools the agent invoked. `0` means measured and none used.                          |
| `mcp_call_count`                                     | integer            | Yes      | **Detail only.** Tool calls that reached external software (MCP servers and webhooks).               |
| `rag_lookup_count`                                   | integer            | Yes      | **Detail only.** Knowledge base lookups.                                                             |
| `tool_result_bytes`                                  | string             | Yes      | **Detail only.** Characters returned by tool results, as a string.                                   |
| `telemetry_fetched_at`                               | string (date-time) | Yes      | **Detail only.** When telemetry was last fetched.                                                    |
| `dnc_scanned_at`                                     | string (date-time) | Yes      | **Detail only.** When the transcript was last scanned for an opt-out request.                        |
| `first_response_ms`                                  | integer            | Yes      | **Detail only.** Reserved; not currently populated.                                                  |

### Transfer

| Field               | Type               | Nullable | Description                                                                                                |
| ------------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------- |
| `transferred_to`    | string             | Yes      | **Detail only.** E.164 number of the person the call was transferred to.                                   |
| `transferred_at`    | string (date-time) | Yes      | **Detail only.** When the warm transfer started.                                                           |
| `transfer_call_sid` | string             | Yes      | **Detail only.** Carrier identifier of the leg dialed to that person. Cleared if the transfer is canceled. |

## Status lifecycle

```mermaid
stateDiagram-v2
    state "in-progress" as in_progress
    state "no-answer" as no_answer
    [*] --> queued: POST /api/calls or campaign dial
    [*] --> in_progress: inbound call answered
    queued --> ringing: provider accepted
    queued --> failed: dial rejected or stuck 15 min
    ringing --> in_progress: carrier reports answered
    ringing --> completed: conversation ended
    ringing --> failed
    ringing --> no_answer
    ringing --> busy
    ringing --> canceled: hung up before answer
    in_progress --> completed
    in_progress --> failed
    failed --> completed: late transcript proves a conversation
    no_answer --> completed: late transcript
    completed --> [*]
    failed --> [*]
    no_answer --> [*]
    busy --> [*]
    canceled --> [*]
```

| Status        | Terminal | Set when                                                                                                                                                                                        |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queued`      | No       | The call passed every check and its record was created, just before the provider is asked to dial.                                                                                              |
| `ringing`     | No       | The provider accepted the call. On Jelliu's default outbound path the record **stays `ringing` for the whole conversation**, until it ends.                                                     |
| `in-progress` | No       | The carrier reported the call as answered (only for calls whose carrier events reach Jelliu), and for inbound calls, which are created in this status.                                          |
| `completed`   | Yes      | The provider reported the conversation as done; you hung up a call that had been answered; or the reconciler found it finished.                                                                 |
| `failed`      | Yes      | The provider reported a failed conversation or an unknown dial failure; the dial was rejected before connecting; or a record stayed `queued` for 15 minutes without ever reaching the provider. |
| `no-answer`   | Yes      | The call rang out.                                                                                                                                                                              |
| `busy`        | Yes      | The line was busy.                                                                                                                                                                              |
| `canceled`    | Yes      | You hung up before the call was answered, or hung up a call that never reached the carrier; or the carrier reported it canceled.                                                                |

Terminal statuses never move back to a live status: a late or duplicated event cannot resurrect a closed call. The one upgrade that does happen is to `completed`: if a call was closed as `failed`, `no-answer`, `busy` or `canceled` and the provider later delivers a non-empty transcript for it, the status becomes `completed` because a conversation evidently took place.

Do not wait for `in-progress` to decide that an outbound call was answered. On the default outbound path it is never set: the call goes from `ringing` straight to a terminal status. Use the terminal status, `duration_seconds` and `outcome` instead.

Status values use **hyphens** (`in-progress`, `no-answer`); outcome values use **underscores** (`no_answer`). They are different fields with different vocabularies.

## Outcomes

`outcome` says what the call accomplished, independently of how it ended technically. It is null until analysis is available, which can be seconds or minutes after the call ends.

| Group               | Values                                                                  |
| ------------------- | ----------------------------------------------------------------------- |
| Sales               | `sale_closed`, `callback_scheduled`, `rejected`                         |
| Reachability        | `no_answer`, `voicemail`, `failed`                                      |
| Handoff             | `escalated_to_human`                                                    |
| Support             | `issue_resolved`, `ticket_created`, `faq_answered`                      |
| Scheduling          | `appointment_booked`, `appointment_rescheduled`, `appointment_canceled` |
| General             | `info_provided`, `follow_up_needed`                                     |
| Collections         | `payment_promised`, `payment_collected`, `payment_refused`              |
| Retention           | `customer_retained`, `customer_reactivated`, `churned`                  |
| Notifications       | `message_delivered`, `message_acknowledged`                             |
| Interviews          | `candidate_evaluated`, `candidate_unqualified`                          |
| Language assessment | `level_assessed`                                                        |

A call counts as **successful** when its outcome is a success outcome for its category:

| Category              | Success outcomes                                                                       |
| --------------------- | -------------------------------------------------------------------------------------- |
| `sales`               | `sale_closed`                                                                          |
| `support`             | `issue_resolved`, `faq_answered`                                                       |
| `scheduling`          | `appointment_booked`, `appointment_rescheduled`                                        |
| `surveys`             | `info_provided`, `issue_resolved`, `callback_scheduled`, `faq_answered`                |
| `collections`         | `payment_promised`, `payment_collected`                                                |
| `retention`           | `customer_retained`, `customer_reactivated`                                            |
| `notifications`       | `message_delivered`, `message_acknowledged`                                            |
| `interview`           | `candidate_evaluated`                                                                  |
| `language_assessment` | `level_assessed`                                                                       |
| `personal`            | `info_provided`                                                                        |
| `general`             | `info_provided`, `sale_closed`, `issue_resolved`, `appointment_booked`, `faq_answered` |

Two outcomes are written outside the analysis:

* `no_answer` when a call ended without a transcript and never connected (status `no-answer`, `busy`, `canceled` or `failed`, or zero duration). A call that did connect but whose transcript is late keeps `outcome: null` until the analysis arrives.
* `escalated_to_human` when you [complete a warm transfer](#transfer-a-live-call-to-a-person), unless the call already had an outcome.

## Durations and time limits

* `duration_seconds` comes from the voice provider when the conversation ends, rounded to whole seconds. If you hang up a call whose carrier leg had already ended, the carrier's duration is recorded right away; if you hang up a live call, the duration arrives with the post-call report a few seconds later.
* `started_at` is the conversation start reported by the provider (for inbound calls, the moment the record is created). `ended_at` is when Jelliu closed the record.

Every voice call has a hard ceiling after which the platform ends it with a short goodbye. The ceiling comes from the agent's category, and the agent's own **maximum call duration** setting (30 to 3600 seconds, see [Agents](/resources/agents)) can only **shorten** it:

| Agent category                     | Target length the agent paces toward | Hard ceiling |
| ---------------------------------- | ------------------------------------ | ------------ |
| `notifications`                    | 60 s                                 | 240 s        |
| `interview`, `language_assessment` | 600 s                                | 900 s        |
| Every other category               | 120 s                                | 480 s        |

During a free trial, each call is additionally capped at the trial minutes you have left (never less than 60 seconds).

If a call ends sooner than expected, read `termination_reason`: it distinguishes a call that hit the maximum duration from one where the agent ended the call itself.

## Transcripts

The detail endpoint returns `transcript` as an array of turns in order:

```json
[
  { "role": "agent", "text": "Hola, ¿hablo con Ana?", "timestamp": "0s", "latency_ms": 820 },
  { "role": "user", "text": "Sí, ella habla.", "timestamp": "3s", "sentiment": 0.1 },
  {
    "role": "agent",
    "text": "Perfecto, le agendo para el jueves a las 10.",
    "timestamp": "41s",
    "tools": [
      { "name": "book_meeting", "type": "webhook", "ok": true, "action_id": "3f7a2c10-8b4e-4d61-9a0c-5e2f1b7d9c44" }
    ]
  }
]
```

| Turn field    | Description                                                                                                         |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| `role`        | `agent` or `user`.                                                                                                  |
| `text`        | What was said.                                                                                                      |
| `timestamp`   | Offset from the start of the call, as a string with an `s` suffix.                                                  |
| `interrupted` | Present and `true` when the turn was cut off.                                                                       |
| `latency_ms`  | Agent turns only: response latency, when measured.                                                                  |
| `sentiment`   | User turns only: `-1` to `1`, when measured.                                                                        |
| `tools`       | Tools the agent called in that turn: `name`, `type`, `ok`, and `action_id` for tools that reached external systems. |

Transcripts and summaries are kept for your plan's retention window (7 days on Starter, 30 on Growth, 90 on Business, unlimited on Enterprise), or less if you set a shorter window. Messages from other channels live in [Conversations](/resources/conversations).

## Common tasks

### Place an outbound call

#### Pick an agent and a destination

You need the `id` of a provisioned agent (see [Agents](/resources/agents)) and either a phone number in E.164 format or the `id` of an existing contact.

#### Create the call

**`curl`**

```bash title="curl"
curl -sS -X POST "https://api.jelliu.co/api/calls" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
    "phoneNumber": "+573001234567"
  }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/calls', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentId: '7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4',
    phoneNumber: '+573001234567',
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);

console.log(body.data.id, body.data.status); // "ringing"
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.post(
    "https://api.jelliu.co/api/calls",
    json={
        "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
        "phoneNumber": "+573001234567",
    },
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}")

print(body["data"]["id"], body["data"]["status"])  # "ringing"
```

A successful request returns `201 Created` with the new record (abridged):

```json
{
  "data": {
    "id": "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10",
    "tenant_id": "e2a4c6d8-1b3f-4a5c-9e7d-0f2b4d6a8c1e",
    "campaign_id": null,
    "contact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
    "phone_number": "+573001234567",
    "status": "ringing",
    "outcome": null,
    "duration_seconds": null,
    "transcript": null,
    "sentiment_score": null,
    "summary": null,
    "started_at": null,
    "ended_at": null,
    "call_direction": "outbound",
    "prompt_variant_id": null,
    "evaluation_criteria_results": {},
    "data_collection_results": {},
    "created_at": "2026-09-14T15:39:02.118Z",
    "updated_at": "2026-09-14T15:39:03.460Z",
    "deleted_at": null
  }
}
```

#### Wait for the result

Subscribe to `call.completed` and `call.failed` [webhooks](/webhooks), or poll `GET /api/calls/{callId}` until `status` is terminal.

#### Request body

**`agentId`** `string (uuid)` — required

The agent that places the call. It must belong to your workspace, must not be paused, and must have finished provisioning.

---

**`phoneNumber`** `string`

Destination in E.164: `+`, a country code that does not start with `0`, and up to 15 digits in total, for example `+573001234567`. Required unless you send `contactId`. When both are sent, `phoneNumber` is the number dialed.

---

**`contactId`** `string (uuid)`

An existing contact to call. Its phone number is used when `phoneNumber` is omitted.

---

**`campaignId`** `string (uuid)`

Files the call under a campaign: it counts toward the campaign's concurrency limit, feeds its reporting, and gives the agent the campaign's context (`product_context`, `target_audience`, campaign name). See [Campaigns](/resources/campaigns).

---

**`promptVariantId`** `string (uuid)`

A/B testing: the prompt variant to attribute this call to.

---

**`promptOverride`** `string`

Replaces the agent's prompt for this call only. Up to 5000 characters. Text that looks like a prompt injection (for example "ignore previous instructions" or "actúa como") is rejected.

---

**`firstMessageOverride`** `string`

Replaces the agent's opening line for this call only. Up to 500 characters, same injection screening.

---

#### What Jelliu does with the destination

* **Contact filing.** The call is always filed under the contact that owns the dialed number. If you omit `contactId`, Jelliu reuses the oldest existing contact with that number, or creates a contact without a campaign. If you send a `contactId` whose number differs from `phoneNumber`, the call is filed under the contact at `phoneNumber` instead.
* **Campaign consistency.** With a `campaignId` and a `phoneNumber` that differs from the named contact's, the number must itself be a contact of that campaign; otherwise the call is refused with `400 VALIDATION_FAILED`: `El número de destino no pertenece a esta campaña. Agrégalo como contacto de la campaña o haz la llamada sin campaña.`
* **Attempt counting.** The contact's `call_attempts` increases only after the provider accepts the call, so failures before dialing do not use up compliance attempts.
* **Context.** The agent receives the contact's name and what earlier interactions with this person captured, on any channel, so it does not greet a returning customer as a stranger.
* **Disclosures.** When the destination requires it, a recording notice is added to the agent's greeting. A greeting that already announces recording is left as it is.
* **Caller ID.** Calls are placed from your workspace's own number when one is provisioned. During a trial without one, a shared platform number is used. On an active paid plan with no provisioned number the call fails with `PHONE_NUMBER_REQUIRED`; see [Phone numbers](/resources/phone-numbers).

### Compliance checks

Every outbound call, including campaign calls, is checked against the compliance rules for the destination's country. The country is read from the number's prefix (`+57` Colombia, `+52` Mexico, `+55` Brazil, `+1` United States, `+54` Argentina, `+56` Chile, `+51` Peru; anything else uses the default profile).

| Check                                    | Default                                                 | Refusal message                                           |
| ---------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
| Allowed calling hours                    | 08:00 to 20:00 (21:00 for Mexico and the United States) | `Outside allowed call hours (08:00-20:00 America/Bogota)` |
| Blocked prefixes you configured          | None                                                    | `Phone number matches blocked prefix: +5760`              |
| Opt-out suppression list                 | Always on                                               | `Phone number is on the opt-out suppression list`         |
| Contact marked do-not-call               | Always on when a contact is resolved                    | `Contact is on the Do Not Call list`                      |
| Number marked do-not-call on any contact | On                                                      | `Phone number is on the Do Not Call list`                 |
| Attempts to the contact today            | 3 per day                                               | `Max daily call attempts reached (3/day)`                 |
| Total attempts to the contact            | 10 (8 for Brazil, 15 for the United States)             | `Max total call attempts reached (10 total)`              |

All of them return `403 COMPLIANCE_BLOCKED`. You can change the per-country values in **Settings → Compliance** or through `/api/compliance/configs`.

The calling-hours window is evaluated in your **workspace's time zone** when one is set in your settings, not in the time zone of the country being called. A workspace in Bogotá calling Mexico City is checked against 08:00 to 21:00 Bogotá time. Only when no workspace time zone is set does the country's own zone apply (Bogotá for numbers outside the modeled countries).

**Destination countries.** Independently of the person being called, the destination itself must be allowed:

* Premium-rate and high-risk ranges (for example `+1900`, `+53`, satellite `+881` to `+883`, and many Caribbean `+1` area codes) are always refused: `Calls to premium-rate or high-risk numbers are blocked (toll fraud protection)`.
* If you configured allowed calling destinations in **Settings → Compliance**, only those are reachable.
* Otherwise the allowed set is Colombia, Mexico, Brazil, United States and Canada (`+1`), Argentina, Chile, Peru and Spain, plus the country you chose at onboarding and the countries of your own voice numbers. Anything else is refused with `This destination is outside the countries this workspace calls. Add it to the allowed calling destinations in Settings → Compliance.`

Each refusal is written to your audit log with the masked number.

**Abuse protection.** A workspace can call the same number at most 5 times per hour (`429 CALL_SPAM_DETECTED`, `Phone number called 6 times in the last hour (max 5)`), and place at most 500 calls per hour in total. Trial workspaces calling a number already called by other trial workspaces can be refused with `403 TRIAL_ABUSE_DETECTED`.

An opt-out is total. When a person says they do not want to be called again, Jelliu adds their number to the suppression list, marks the contact do-not-call and never dials them again, from any campaign or contact record. See [Contacts](/resources/contacts).

### List calls

`GET /api/calls` uses [timestamp-cursor pagination](/pagination#timestamp-cursor). Filter with `agentId` and `campaignId`.

**`curl`**

```bash title="curl"
curl -sS "https://api.jelliu.co/api/calls?limit=50&campaignId=0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const url = new URL('https://api.jelliu.co/api/calls');
url.searchParams.set('limit', '50');
url.searchParams.set('campaignId', '0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90');

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});
const { data } = await res.json();

for (const call of data.calls) {
  console.log(call.id, call.status, call.outcome, call.duration_seconds);
}
console.log('next page:', data.nextCursor);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.get(
    "https://api.jelliu.co/api/calls",
    params={"limit": 50, "campaignId": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90"},
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
data = res.json()["data"]

for call in data["calls"]:
    print(call["id"], call["status"], call["outcome"], call["duration_seconds"])
print("next page:", data["nextCursor"])
```

```json
{
  "data": {
    "calls": [
      {
        "id": "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10",
        "agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
        "agent_name": "Valentina",
        "campaign_id": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
        "campaign_name": "Webinar follow-up",
        "contact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
        "contact_name": "Ana Gómez",
        "phone_number": "+573001234567",
        "call_direction": "outbound",
        "status": "completed",
        "outcome": "callback_scheduled",
        "duration_seconds": 184,
        "sentiment_score": 0.6,
        "summary": "La clienta pidió que la llamen el jueves en la mañana.",
        "termination_reason": "end_call tool was called.",
        "main_language": "es",
        "cost_usd": "0.3912",
        "has_recording": true,
        "analysis": {
          "summary": "La clienta pidió que la llamen el jueves en la mañana.",
          "outcome": "callback_scheduled",
          "sentiment": "positive",
          "sentiment_score": 0.6,
          "key_topics": [],
          "qualification": {
            "score": 85,
            "interest": "high",
            "answered": true,
            "next_step": "Llamar el jueves a las 10:00",
            "objections": [],
            "reasons": ["Interes high.", "Quedo un siguiente paso concreto: Llamar el jueves a las 10:00."]
          }
        },
        "started_at": "2026-09-14T15:39:05.000Z",
        "ended_at": "2026-09-14T15:42:09.771Z",
        "created_at": "2026-09-14T15:39:02.118Z",
        "updated_at": "2026-09-14T15:42:11.034Z"
      }
    ],
    "total": 1284,
    "limit": 50,
    "offset": 0,
    "nextCursor": "2026-09-14T15:39:02.118Z"
  }
}
```

List responses are cached for up to 60 seconds and call details for up to 5 minutes. Status changes Jelliu applies itself (hang-ups, post-call reports, the reconciler) clear the cache, so a freshly ended call normally shows up right away.

### Get one call

**`curl`**

```bash title="curl"
curl -sS "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const callId = '5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10';
const res = await fetch(`https://api.jelliu.co/api/calls/${callId}`, {
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});
const { data: call } = await res.json();

console.log(call.status, call.outcome);
for (const turn of call.transcript ?? []) {
  console.log(`[${turn.timestamp}] ${turn.role}: ${turn.text}`);
}
```

**`Python`**

```python title="Python"
import os
import requests

call_id = "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10"
res = requests.get(
    f"https://api.jelliu.co/api/calls/{call_id}",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
call = res.json()["data"]

print(call["status"], call["outcome"])
for turn in call.get("transcript") or []:
    print(f"[{turn['timestamp']}] {turn['role']}: {turn['text']}")
```

The response is `{ "data": { ... } }` with every field in [the call object](#the-call-object), including `transcript`.

### Download the recording

`GET /api/calls/{callId}/audio` streams the recording as binary audio (`Content-Type` from the provider, `audio/mpeg` by default). Check `has_recording` first: it is `true` once the call is over and has audio to play.

**`curl`**

```bash title="curl"
curl -sS -f -o call.mp3 \
  "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/audio" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
import { writeFile } from 'node:fs/promises';

const callId = '5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10';
const res = await fetch(`https://api.jelliu.co/api/calls/${callId}/audio`, {
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});

if (res.status === 404) {
  // "processing": retry in a minute. "expired": the audio is gone for good.
  const state = res.headers.get('X-Recording-Status');
  const { error } = await res.json();
  throw new Error(`${state ?? 'unavailable'}: ${error.message}`);
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);

await writeFile('call.mp3', Buffer.from(await res.arrayBuffer()));
```

**`Python`**

```python title="Python"
import os
import requests

call_id = "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10"
res = requests.get(
    f"https://api.jelliu.co/api/calls/{call_id}/audio",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    stream=True,
    timeout=60,
)

if res.status_code == 404:
    state = res.headers.get("X-Recording-Status")  # "processing" or "expired"
    raise RuntimeError(f"{state or 'unavailable'}: {res.json()['error']['message']}")
res.raise_for_status()

with open("call.mp3", "wb") as f:
    for chunk in res.iter_content(chunk_size=64 * 1024):
        f.write(chunk)
```

A missing recording is always a `404` with code `CALL_NOT_FOUND`. The `X-Recording-Status` response header tells the two common cases apart:

| `X-Recording-Status` | Message                                                                                                                                   | What to do                                                               |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `processing`         | `Recording is still processing — try again in a minute.`                                                                                  | Retry shortly. Recordings become available a few minutes after the call. |
| `expired`            | `This recording is no longer available — recordings are kept by the voice provider for a limited time and this call is past that window.` | Nothing: the audio has been deleted at the source.                       |
| (absent)             | `Call has no recording (no conversation_id)`                                                                                              | The call never started a conversation.                                   |

Audio is retained by the voice provider for your plan's window: 7 days on Starter, 30 on Growth, 90 on Business, and until you delete it on Enterprise. Download recordings you need to keep. Responses carry `Cache-Control: private, max-age=300`.

### Hang up a call

`POST /api/calls/{callId}/hangup` ends a live call. It is idempotent: calling it on a call that has already ended does nothing and still returns `200`.

**`curl`**

```bash title="curl"
curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/hangup" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const callId = '5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10';
const res = await fetch(`https://api.jelliu.co/api/calls/${callId}/hangup`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}`);
```

**`Python`**

```python title="Python"
import os
import requests

call_id = "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10"
res = requests.post(
    f"https://api.jelliu.co/api/calls/{call_id}/hangup",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
res.raise_for_status()
```

```json
{
  "data": { "success": true }
}
```

Jelliu first asks the carrier for the call's real state, then records the right terminal status:

| Carrier state at hang-up                | Recorded status                                                                  |
| --------------------------------------- | -------------------------------------------------------------------------------- |
| Already ended (for example it rang out) | The carrier's own status and duration, for example `no-answer`.                  |
| Answered and talking                    | `completed`. Duration, transcript and analysis follow with the post-call report. |
| Not yet answered                        | `canceled`.                                                                      |
| The call never reached the carrier      | `canceled`.                                                                      |

Hanging up ends the telephone line. The concurrency slot is released immediately; the post-call report still arrives and completes the record.

### Transfer a live call to a person

A **warm transfer** brings a person into a live call. The caller is moved into a conference, Jelliu dials the person you name from your workspace's number, and both end up on the line. You then either **complete** the transfer (the person keeps the call) or **cancel** it (the person's leg is hung up and the AI agent picks the conversation back up).

These three endpoints are admin operations and require a key with the `full` scope.

#### Start the transfer

**`curl`**

```bash title="curl"
curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/transfer" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "transfer_to": "+5716012345" }'
```

**`Node.js`**

```javascript title="Node.js"
const callId = '5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10';
const res = await fetch(`https://api.jelliu.co/api/calls/${callId}/transfer`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ transfer_to: '+5716012345' }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);

console.log(body.data.transferId, body.data.conferenceId);
```

**`Python`**

```python title="Python"
import os
import requests

call_id = "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10"
res = requests.post(
    f"https://api.jelliu.co/api/calls/{call_id}/transfer",
    json={"transfer_to": "+5716012345"},
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}: {body['error']['message']}")

print(body["data"]["transferId"], body["data"]["conferenceId"])
```

```json
{
  "data": {
    "transferId": "CA8f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c",
    "conferenceId": "CF1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
  }
}
```

`transfer_to` must be an E.164 number and passes the same [destination rules](#compliance-checks) as outbound calls. The call must be live on the carrier: Jelliu checks the carrier directly, not the stored status.

#### Complete or cancel it

**`curl`**

```bash title="curl"
# The person keeps the call
curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/transfer/complete" \
  -H "Authorization: Bearer $JELLIU_API_KEY"

# Or: hang up the person's leg and give the call back to the agent
curl -sS -X POST "https://api.jelliu.co/api/calls/5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10/transfer/cancel" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const callId = '5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10';
const action = 'complete'; // or 'cancel'

const res = await fetch(`https://api.jelliu.co/api/calls/${callId}/transfer/${action}`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error.code}`);

console.log(body.message);
```

**`Python`**

```python title="Python"
import os
import requests

call_id = "5b0d2f7e-9a41-4c3e-8f0a-2c6d1e7b9a10"
action = "complete"  # or "cancel"

res = requests.post(
    f"https://api.jelliu.co/api/calls/{call_id}/transfer/{action}",
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

print(body["message"])
```

These two endpoints answer **without** a `data` wrapper:

```json
{ "success": true, "message": "Transfer completed — AI agent disconnected" }
```

```json
{ "success": true, "message": "Transfer canceled — AI agent resumed" }
```

What each step does to the call:

* **Complete** does not hang up anyone. It sets `outcome` to `escalated_to_human` if the call had no outcome yet, and the record closes normally when the caller hangs up.
* **Cancel** hangs up the person's leg, starts a new agent conversation on the caller's line and clears `transferred_to`, `transferred_at` and `transfer_call_sid`. It fails if the call's agent is paused or no longer provisioned.

Agents can also hand calls to a person on their own, through a transfer tool configured on the agent. That transfer is executed by the voice provider during the conversation; the destination number is checked against the same destination rules when you save it on the agent. See [Agents](/resources/agents).

## Inbound calls

When someone calls one of your numbers, Jelliu admits the call only if your workspace is not suspended, your plan allows calls and a concurrency slot is free. Then it picks the agent:

1. **The agent assigned to the number.** If that agent is paused, the call is **not** passed to another agent: pausing the assigned agent stops that number from answering.
2. **Routing among your agents**, choosing the least busy provisioned agent that is not paused.
3. **Your oldest provisioned, unpaused agent**, as a last resort.

Numbers that were removed (soft-deleted) never route calls. An IVR menu, when enabled on the number, is played before routing. Inbound call records are created with `call_direction: "inbound"`, status `in-progress` and no campaign. Assign agents and configure IVR on the number; see [Phone numbers](/resources/phone-numbers).

## Retries

Standalone calls placed with `POST /api/calls` are never retried. Calls that belong to a campaign and end `no-answer`, `busy`, or without connecting are re-dialed according to the campaign's `maxRetryAttempts` and `retryIntervalMinutes`, and each retry passes all the checks on this page again. See [Campaigns](/resources/campaigns).

## Errors

All errors use the standard [error envelope](/errors). Codes you can receive from the calls endpoints:

| Code                           | Status | When                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_FAILED`            | 400    | The body failed validation (`Invalid call input`, with `details.fieldErrors`); neither `phoneNumber` nor `contactId` was sent; the number is not E.164 (`Invalid or missing phone number (E.164 required)`); the destination is not a contact of the named campaign; a `callId` is not a UUID (`Invalid call ID`); list parameters are out of range (`Invalid calls query`). |
| `VALIDATION_FAILED`            | 400    | Transfers: `Invalid transfer input`, `Call has no active session — cannot transfer`, `Call must be in-progress to initiate a transfer`, `No active transfer for this call`, `No active transfer to cancel`.                                                                                                                                                                  |
| `VALIDATION_FAILED`            | 409    | Transfers: `Could not confirm the call is still live — try again in a moment`.                                                                                                                                                                                                                                                                                               |
| `UNAUTHORIZED`                 | 401    | Missing or invalid API key. See [Authentication](/authentication).                                                                                                                                                                                                                                                                                                           |
| `FORBIDDEN`                    | 403    | The key lacks `write` (placing calls, hanging up) or `full` (transfers).                                                                                                                                                                                                                                                                                                     |
| `BILLING_ERROR`                | 403    | No active plan, trial minutes used up, or the plan's overage ceiling was reached.                                                                                                                                                                                                                                                                                            |
| `COMPLIANCE_BLOCKED`           | 403    | Outside calling hours, opt-out, do-not-call, attempt limits, blocked prefix, or a destination that is not allowed. The `message` names the rule.                                                                                                                                                                                                                             |
| `TRIAL_ABUSE_DETECTED`         | 403    | A trial workspace called a number already used by other trial workspaces.                                                                                                                                                                                                                                                                                                    |
| `PHONE_NUMBER_REQUIRED`        | 403    | An active paid plan has no provisioned phone number to call from.                                                                                                                                                                                                                                                                                                            |
| `NOT_FOUND`                    | 404    | `contactId` does not exist and no `phoneNumber` was sent (`Contact not found`).                                                                                                                                                                                                                                                                                              |
| `AGENT_NOT_FOUND`              | 404    | The agent does not exist in your workspace, or was deleted.                                                                                                                                                                                                                                                                                                                  |
| `CALL_NOT_FOUND`               | 404    | The call does not exist in your workspace, or its recording is unavailable (see [Download the recording](#download-the-recording)).                                                                                                                                                                                                                                          |
| `AGENT_PAUSED`                 | 409    | The agent is paused. `503` when its state could not be read; the call is refused as a precaution.                                                                                                                                                                                                                                                                            |
| `AGENT_PROVISIONING`           | 409    | The agent is still being set up. Retry after `Retry-After` (5 seconds).                                                                                                                                                                                                                                                                                                      |
| `NUMBER_UNVERIFIED`            | 400    | The telephony account cannot dial this destination (trial telephony account or a region not enabled). The record is marked `failed`.                                                                                                                                                                                                                                         |
| `CALL_SPAM_DETECTED`           | 429    | More than 5 calls to the same number in an hour, or more than 500 calls per hour for the workspace.                                                                                                                                                                                                                                                                          |
| `MAX_CONCURRENT_CALLS_REACHED` | 429    | Your plan's concurrent-call limit, or the campaign's `maxConcurrentCalls`, is in use. No `Retry-After` header; retry when a call ends.                                                                                                                                                                                                                                       |
| `RATE_LIMIT_EXCEEDED`          | 429    | More than 20 requests per minute under `/api/calls`. Honor `Retry-After`.                                                                                                                                                                                                                                                                                                    |
| `CALL_INITIATION_FAILED`       | 502    | The voice provider rejected the call. The body carries only `Internal server error`; the record is marked `failed` and can be inspected with `GET /api/calls`.                                                                                                                                                                                                               |
| `TELEPHONY_ERROR`              | 502    | The carrier failed while starting, completing or canceling a transfer (also when canceling is refused because the agent is paused). The body carries `Internal server error`.                                                                                                                                                                                                |

Do not retry `CALL_INITIATION_FAILED` or a timeout on `POST /api/calls` blindly. If the provider accepted the call but a later bookkeeping step failed, Jelliu still returns `201` for the live call; a network timeout on your side may hide a call that is already ringing. List recent calls for the number before dialing again.

## Limits

| Limit               | Value                                                                                                                                                                                                                                                          |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Request rate        | 20 requests per minute per workspace for everything under `/api/calls`, reads included. Transfers also count against the general per-plan limit. This limit fails closed. See [Rate limits](/rate-limits).                                                     |
| Concurrent calls    | Starter 3, Growth 10, Business 25, Enterprise custom (subject to a platform-wide ceiling). Inbound and outbound calls share the same slots. A campaign's `maxConcurrentCalls` further caps calls filed under that campaign.                                    |
| Calls to one number | 5 per hour per workspace.                                                                                                                                                                                                                                      |
| Calls per workspace | 500 per hour.                                                                                                                                                                                                                                                  |
| Minutes             | Trial: 15 minutes in total, and each call is capped at the minutes left. Paid plans: calls are refused once usage reaches 150% of the plan's included minutes plus any minute packs bought, until the period renews, a pack is bought or the plan is upgraded. |
| Call length         | 240 to 900 seconds depending on the agent's category; see [Durations and time limits](#durations-and-time-limits).                                                                                                                                             |
| Recording retention | 7, 30 or 90 days on Starter, Growth and Business; until deleted on Enterprise.                                                                                                                                                                                 |

| Endpoint                                                                            | Scope   |
| ----------------------------------------------------------------------------------- | ------- |
| `GET /api/calls`, `GET /api/calls/{callId}`, `GET /api/calls/{callId}/audio`        | `read`  |
| `POST /api/calls`, `POST /api/calls/{callId}/hangup`                                | `write` |
| `POST /api/calls/{callId}/transfer`, `.../transfer/complete`, `.../transfer/cancel` | `full`  |

## Webhooks

| Event                                   | Sent when                                                                                                                                                                                           |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `call.completed`                        | A call ended and its analysis is available. When the analysis is still being computed, the event waits for it so that `outcome` is filled in.                                                       |
| `call.failed`                           | A call ended in a technical failure or could not connect. When the background reconciler closed the call because its end was never reported, the payload is smaller and carries `reconciled: true`. |
| `crm_sync.completed`, `crm_sync.failed` | The call was written, or failed to be written, to a connected CRM.                                                                                                                                  |
| `agent.action_recorded`                 | The agent invoked a tool during the call.                                                                                                                                                           |

`call.started`, `call.recording_ready` and `call.transcript_ready` are accepted in subscriptions but are not sent yet. See [Webhooks](/webhooks#payload) for payloads, signatures and retries.

In `call.completed` and `call.failed`, `sentimentScore` is `0` when sentiment was not measured, while the API returns `sentiment_score: null` for the same call. Read the call from the API when you need to tell "neutral" from "not measured".

## Related

#### [Agents](/resources/agents)

Create and configure the agents that place and answer calls.

#### [Campaigns](/resources/campaigns)

Dial many contacts on a schedule, with retries and concurrency limits.

#### [Phone numbers](/resources/phone-numbers)

Provision numbers, assign agents and configure inbound routing.

#### [Contacts](/resources/contacts)

The people you call, their opt-outs and suppression.

#### [Conversations](/resources/conversations)

WhatsApp, email and web chat conversations with the same agents.

#### [Webhooks](/webhooks)

Get notified when calls end.

#### [API reference](/api-reference)

Every calls endpoint, parameter and response.