Skip to content

Calls ​

Your organization's calls: every call it placed or took, with its outcome once one has been filed. Two routes:

  • GET /api/public/v1/calls — a page of calls, newest first, or a change feed with updated_since
  • GET /api/public/v1/calls/{id} — one call, by the id a webhook or the list gave you

Use them beside the webhooks, not instead of them. call.completed and call_outcome.submitted tell you the moment something happens; these routes let you backfill history from before you subscribed, and catch anything a delivery missed. A call here uses the same field names as a webhook's call namespace, so one upsert by id serves both.

PropertyValue
MethodGET
Scopescall:read
Org kindSeller organizations only
Cost1 token per request, whatever limit
BodyNone

call:read is in the default scope set, so a key minted without an explicit scope list can already call these.

Organization kind ​

Calls belong to the organization whose agents and numbers handled them — the seller side, as in the dashboard. A buyer-only organization gets 403:

json
{
  "error": {
    "code": "FORBIDDEN",
    "message": "This endpoint is only available to seller organizations."
  }
}

An organization that is both buyer and seller is served normally.

The call object ​

FieldTypeNullableExample
idstring (uuid)No"5c9d3a70-1e84-4b2f-9c6a-8d0f1e3b7a25"
statusstringNo"ended"
directionstringNo"outbound"
from_e164stringNo"+27821234567"
to_e164stringNo"+27829876543"
duration_secnumberYes42
started_atstring (ISO)Yes"2026-08-17T09:14:00.000Z"
answered_atstring (ISO)Yes"2026-08-17T09:14:10.000Z"
ended_atstring (ISO)Yes"2026-08-17T09:15:00.000Z"
campaign_idstring (uuid)Yes"7e8f9a0b-1c2d-4e3f-a4b5-c6d7e8f9a0b1"
contact_idstring (uuid)Yes"8b7a6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d"
agent_kindstringNo"human"
outcomeobjectYessee below
created_atstring (ISO)No"2026-08-17T09:13:58.000Z"
updated_atstring (ISO)No"2026-08-17T09:15:01.000Z"

outcome, once a disposition has been filed, and null until then:

FieldTypeNullableExample
outcome.idstring (uuid)No"2d4f6a80-3b5c-4d7e-8f9a-0b1c2d3e4f50"
outcome.dispositionstringNo"interested"
outcome.callback_atstring (ISO)Yes"2026-08-20T08:00:00.000Z"
outcome.submitted_atstring (ISO)No"2026-08-17T09:15:00.000Z"

The fields are defined by ApiCallSchema.resource in src/lib/schema/api/api_call.schema.ts. Everything a field means on call.completed and call_outcome.submitted it means here, with three differences worth knowing:

  • status can be any status, not only a terminal one. A call still in progress is listed too. The values are queued, initiated, ringing and in_progress while it is live, then one of the three terminal ones — ended, missed or failed (CALLS.STATUS.CURRENT_IDS in src/lib/const/call.const.ts, and call.completed says what each means). A call that is not terminal yet will change — sync it again. The status says only where the call is; how it went (answered, busy, voicemail) is outcome.disposition. Until 22 October 2026 a finished call read completed, busy, no_answer or canceled; those calls now read ended, or missed for an inbound call nobody answered, and the change moved their updated_at, so a sync by updated_since picks it up once.
  • contact_id is the contact's id only, not the contact namespace a webhook carries. It is null when the call reached nobody your organization holds a contact record for — an inbound call, a click-to-call, or a campaign whose contacts belong to another organization.
  • agent_kind is the webhook's agent.kind: ai when a voice AI agent placed the call, human otherwise — including a call nobody answered. human means "not a voice AI agent", not "a person spoke".

outcome.disposition is one of interested, not_interested, callback, dnq, language_barrier, do_not_call, transferred, hung_up, wrong_number, deceased, voicemail, straight_to_voicemail, no_answer, declined, busy and invalid_number (CAMPAIGNS.DISPOSITION.CURRENT_IDS in src/lib/const/campaign.const.ts; the call_outcome.submitted event says what each means). transferred is a voice AI agent's call put through to a person at one of the campaign's transfer numbers; such a call keeps agent_kind: "ai". Until 22 October 2026 there was also dropped; those calls now read hung_up when somebody answered and declined when nobody did, and the change moved their updated_at, so a sync by updated_since picks it up once. The agent's free-text notes are not published.

An agent's practice calls — the microphone check — are never listed.

GET /api/public/v1/calls ​

Query parameters ​

ParameterDefaultMeaning
limit50Calls per page, 1–100.
cursor—The next_cursor from the previous page.
updated_since—ISO 8601 with an offset. Turns the list into a change feed.

Paging is the same as every list's; see Pagination. Any other parameter is a 400.

Two orders ​

Without updated_since, calls come newest first, by when the call was created. This is the list to read "what happened recently", or to backfill everything once.

With updated_since, the list returns every call changed at or after that instant, oldest change first — the order the sync recipe relies on. A call counts as changed when anything on it changes, its outcome included: filing a disposition moves the call's updated_at. So does work that touches no field listed above — the call's price arriving, its recording being archived — so expect to see a call again shortly after it ends, unchanged. Upsert by id and that is harmless.

A cursor carries its place in one order only. Do not take a cursor from one order and send it with the other.

Request ​

bash
curl -sS "https://<your-app-domain>/api/public/v1/calls?updated_since=2026-08-17T09:00:00Z&limit=100" \
  -H "authorization: Bearer $CALL_CENTER_API_KEY"

Response ​

200 OK

json
{
  "data": [
    {
      "id": "5c9d3a70-1e84-4b2f-9c6a-8d0f1e3b7a25",
      "status": "ended",
      "direction": "outbound",
      "from_e164": "+27821234567",
      "to_e164": "+27829876543",
      "duration_sec": 42,
      "started_at": "2026-08-17T09:14:00.000Z",
      "answered_at": "2026-08-17T09:14:10.000Z",
      "ended_at": "2026-08-17T09:15:00.000Z",
      "campaign_id": "7e8f9a0b-1c2d-4e3f-a4b5-c6d7e8f9a0b1",
      "contact_id": "8b7a6c5d-4e3f-4a2b-9c8d-7e6f5a4b3c2d",
      "agent_kind": "human",
      "outcome": {
        "id": "2d4f6a80-3b5c-4d7e-8f9a-0b1c2d3e4f50",
        "disposition": "callback",
        "callback_at": "2026-08-20T08:00:00.000Z",
        "submitted_at": "2026-08-17T09:15:30.000Z"
      },
      "created_at": "2026-08-17T09:13:58.000Z",
      "updated_at": "2026-08-17T09:15:30.000Z"
    }
  ],
  "pagination": {
    "limit": 100,
    "has_more": false,
    "next_cursor": null
  }
}

GET /api/public/v1/calls/{id} ​

One call, as a single resource in data. A call of another organization, a practice call, and an id that is not a UUID all answer the same 404, so the answer never reveals whether some other organization holds that id:

json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "No such call."
  }
}
bash
curl -sS https://<your-app-domain>/api/public/v1/calls/5c9d3a70-1e84-4b2f-9c6a-8d0f1e3b7a25 \
  -H "authorization: Bearer $CALL_CENTER_API_KEY"

Errors ​

SituationStatusCode
Unknown query parameter400INVALID_INPUT
limit outside 1–100400INVALID_INPUT
updated_since not ISO 8601, or with no offset400INVALID_INPUT
A cursor this API did not issue400INVALID_INPUT
Key lacks call:read403FORBIDDEN
Buyer-only organization403FORBIDDEN
No such call in your organization404NOT_FOUND
Budget exhausted429TOO_MANY_REQUESTS

Suggested sync loop ​

checkpoint = stored value, or the first instant you care about
loop:
    page through GET /calls?updated_since=<checkpoint>&limit=100
        upsert each call by id
        remember the largest updated_at seen
    checkpoint = largest updated_at seen, minus one minute
    sleep, then loop
on 429 -> sleep retry-after, retry the same page

Run it beside your webhook endpoint rather than instead of it: the webhook is how you hear of a call within seconds, this loop is how you know you never missed one.