Skip to content

Calls ​

16 endpoints. Every path below is relative to the API base URL and already includes the /api/v1 prefix — see conventions for authentication, pagination, errors and idempotency.

Endpoint
GET/callsList the organization's calls with filter, sort and pagination, returning a summary row per call plus page meta
POST/callsPlace an assistant-driven outbound call and return the created call detail, idempotent on a repeated key
GET/calls/{call}Fetch one call in full, including legs, recording state, metadata, variables and analysis fields
DELETE/calls/{call}Delete a call record and return the deleted id
GET/calls/{call}/artifactsList the stored artifacts for a call with type, status, size, checksum and purge state
GET/calls/{call}/artifacts/{artifact}/downloadSigned fallback download used when the storage driver cannot sign a URL itself
GET/calls/{call}/costsBreak a call's cost down by component and provider, alongside the per-category totals stored on the call
GET/calls/{call}/debugExport a sanitized diagnostic bundle for a call with the assistant snapshot, legs, events, metrics and costs
POST/calls/{call}/endEnd an in-progress call through the state machine and return the resulting call summary
GET/calls/{call}/eventsList a call's lifecycle events, optionally filtered by type or restricted to errors
GET/calls/{call}/messagesList the conversation turns of a call with role, timing, tool calls and transcription confidence
GET/calls/{call}/metricsList a call's per-turn latency metrics alongside the aggregate STT, LLM, TTS and end-to-end summary
GET/calls/{call}/recordingReturn a short-lived signed URL for a call's best available recording plus its duration, channels and MIME type
DELETE/calls/{call}/recordingPurge a call's audio artifacts, leaving the artifact rows as evidence they existed
GET/calls/{call}/transcriptReturn a call's full transcript text with flags saying whether it was redacted or purged
GET/calls/searchSearch calls using the same filters, sorting and pagination as GET /calls

Endpoints ​

List the organization's calls with filter, sort and pagination, returning a summary row per call plus page meta ​

GET/calls
  • Requires call.read.
  • Only whitelisted filters are read; unknown query params and unknown sort columns are ignored silently rather than rejected
bash
curl -X GET "$CALLAGENT_API_URL/calls" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Errors

StatusWhen it happens
403The key or user lacks the call.read scope

Example response, generated from the schema:

json
{
  "data": [
    {
      "id": null,
      "status": "string",
      "direction": "string",
      "transport": "string",
      "from": "+40712345678",
      "to": "+40712345678",
      "assistant_id": "01JQ0000000000000000000000",
      "customer_id": "01JQ0000000000000000000000",
      "queue_id": "01JQ0000000000000000000000",
      "operator_id": "01JQ0000000000000000000000",
      "provider": "string",
      "started_at": "2026-01-01T12:00:00+00:00",
      "connected_at": "2026-01-01T12:00:00+00:00",
      "ended_at": "2026-01-01T12:00:00+00:00",
      "duration_seconds": 30,
      "billable_seconds": 30,
      "ended_reason": "string",
      "cost_total": "string",
      "cost_currency": "string",
      "latency": {},
      "created_at": "2026-01-01T12:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 1,
    "last_page": 1
  }
}

Place an assistant-driven outbound call and return the created call detail, idempotent on a repeated key ​

POST/calls
  • Requires call.create.
  • Accepts an Idempotency-Key header; replaying a key returns the original result instead of acting twice.
  • A carrier or policy refusal is NOT an error status: the response is still 201 with status/ended_reason showing the failure

Request body

FieldTypeRequiredNotes
tostringyesmax 128 chars
fromstring | null—max 128 chars
assistant_idstring | null—
assistant_version_idstring | null—
assistant_overridesstring[] | null—
squad_idstring | null—
phone_number_idstring | null—
sip_trunk_idstring | null—
customer_idstring | null—
campaign_idstring | null—
transportpstn · sip · web · mobile · null | null—
providerstring | null—max 32 chars
variablesstring[] | null—
metadatastring[] | null—
idempotency_keystring | null—max 128 chars
answering_machine_detectionboolean | null—Outbound-only. Omitted, both follow the assistant and the platform default: AMD is asked of the carrier when the assistant's voicemail plan wants a provider verdict, and the ring timeout comes from telephony.defaults.outbound_ring_timeout.
ring_timeout_secondsinteger | null—min 5. max 300
bash
curl -X POST "$CALLAGENT_API_URL/calls" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
       "to": "+40712345678",
       "from": "+40712345678",
       "assistant_id": "01JQ0000000000000000000000",
       "assistant_version_id": "01JQ0000000000000000000000",
       "squad_id": "01JQ0000000000000000000000",
       "phone_number_id": "01JQ0000000000000000000000",
       "sip_trunk_id": "01JQ0000000000000000000000"
     }'
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "to": "+40712345678",
    "from": "+40712345678",
    "assistant_id": "01JQ0000000000000000000000",
    "assistant_version_id": "01JQ0000000000000000000000",
    "squad_id": "01JQ0000000000000000000000",
    "phone_number_id": "01JQ0000000000000000000000",
    "sip_trunk_id": "01JQ0000000000000000000000"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
    'to' => '+40712345678',
    'from' => '+40712345678',
    'assistant_id' => '01JQ0000000000000000000000',
    'assistant_version_id' => '01JQ0000000000000000000000',
    'squad_id' => '01JQ0000000000000000000000',
    'phone_number_id' => '01JQ0000000000000000000000',
    'sip_trunk_id' => '01JQ0000000000000000000000',
]),
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.post(
    f"{os.environ['CALLAGENT_API_URL']}/calls",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    json={
      "to": "+40712345678",
      "from": "+40712345678",
      "assistant_id": "01JQ0000000000000000000000",
      "assistant_version_id": "01JQ0000000000000000000000",
      "squad_id": "01JQ0000000000000000000000",
      "phone_number_id": "01JQ0000000000000000000000",
      "sip_trunk_id": "01JQ0000000000000000000000"
    },
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
201data-wrapped object

Errors

StatusWhen it happens
403Missing call.create scope, or the API key is restricted to other assistants
422The payload failed validation (to is required; ids must be ULIDs)

Example response, generated from the schema:

json
{
  "data": {}
}

Fetch one call in full, including legs, recording state, metadata, variables and analysis fields ​

GET/calls/{call}
  • Requires call.read.

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped object

Errors

StatusWhen it happens
403The key or user lacks the call.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": {}
}

Delete a call record and return the deleted id ​

DELETE/calls/{call}
  • Requires call.delete.
  • This response is not data-wrapped.
  • Body is a bare {"deleted": true, "id": "..."} object, not the {"data": ...} envelope the rest of the calls API uses

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X DELETE "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000`, {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.delete(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200Bare JSON object, not data-wrapped

Errors

StatusWhen it happens
403The key or user lacks the call.delete scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "deleted": true,
  "id": null
}

List the stored artifacts for a call with type, status, size, checksum and purge state ​

GET/calls/{call}/artifacts
  • Requires call.read.
  • Returns metadata only; no download URL is included. Use GET /calls/{call}/recording or a signed download URL

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/artifacts" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/artifacts`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/artifacts');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/artifacts",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Errors

StatusWhen it happens
403The key or user lacks the call.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": [
    {
      "id": null,
      "type": "string",
      "status": "string",
      "mime_type": "string",
      "size_bytes": "string",
      "duration_seconds": "string",
      "checksum_sha256": "string",
      "channels": "string",
      "created_at": "2026-01-01T12:00:00+00:00",
      "purged_at": "2026-01-01T12:00:00+00:00"
    }
  ]
}

Signed fallback download used when the storage driver cannot sign a URL itself ​

GET/calls/{call}/artifacts/{artifact}/download
  • No scope is enforced in the handler — any valid key may call it.
  • This response is not data-wrapped.
  • Runs without api.key and tenant middleware and queries withoutGlobalScopes: the URL signature is the only authorization

Path parameters

NameTypeRequiredNotes
callstringyes
artifactstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/artifacts/01JQ0000000000000000000000/download" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/artifacts/01JQ0000000000000000000000/download`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/artifacts/01JQ0000000000000000000000/download');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/artifacts/01JQ0000000000000000000000/download",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200Not JSON — text/html

Errors

StatusWhen it happens
403The URL signature is missing, altered or expired
404Unknown call or artifact, or the artifact is not readable

Break a call's cost down by component and provider, alongside the per-category totals stored on the call ​

GET/calls/{call}/costs
  • Requires call.read.
  • The body carries a top-level summary key beside data; a client reading only data misses the totals

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/costs" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/costs`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/costs');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/costs",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Errors

StatusWhen it happens
403The key or user lacks the call.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": [
    {
      "component": "string",
      "provider": "string",
      "model": "string",
      "quantity": "string",
      "unit": "string",
      "unit_cost": "string",
      "cost": "string",
      "currency": "string",
      "billable": true
    }
  ],
  "summary": {
    "total": "string",
    "stt": "string",
    "llm": "string",
    "tts": "string",
    "telephony": "string",
    "other": "string",
    "currency": "string"
  }
}

Export a sanitized diagnostic bundle for a call with the assistant snapshot, legs, events, metrics and costs ​

GET/calls/{call}/debug
  • Requires call.read.
  • This response is not data-wrapped.
  • Returns a bare object (exported_at, schema_version, notice, call, ...) with no data envelope, and sets Content-Disposition: attachment

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/debug" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/debug`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/debug');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/debug",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200Bare JSON object, not data-wrapped

Errors

StatusWhen it happens
403The key or user lacks the call.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "exported_at": "2026-01-01T12:00:00+00:00",
  "schema_version": 1,
  "notice": "string",
  "call": {},
  "assistant_snapshot": {},
  "legs": [
    {
      "id": null,
      "type": "string",
      "direction": "string",
      "status": "string",
      "provider": "string",
      "provider_leg_id": "01JQ0000000000000000000000",
      "from_number": "+40712345678",
      "to_number": "+40712345678",
      "codec": "string",
      "sample_rate": "string",
      "started_at": "2026-01-01T12:00:00+00:00",
      "answered_at": "2026-01-01T12:00:00+00:00",
      "bridged_at": "2026-01-01T12:00:00+00:00",
      "ended_at": "2026-01-01T12:00:00+00:00",
      "ended_reason": "string",
      "quality": {}
    }
  ],
  "messages": [
    {}
  ],
  "events": [
    {}
  ],
  "metrics": [
    {
      "name": "Support Line",
      "component": "string",
      "turn_index": 1,
      "value_ms": 1,
      "value": 1,
      "unit": "string",
      "provider": "string",
      "model": "string",
      "offset_ms": 1,
      "started_at": "2026-01-01T12:00:00+00:00",
      "first_byte_at": "2026-01-01T12:00:00+00:00",
      "completed_at": "2026-01-01T12:00:00+00:00",
      "error_code": "string",
      "error_message": "string"
    }
  ],
  "costs": [
    {
      "component": "string",
      "provider": "string",
      "model": "string",
      "quantity": "string",
      "unit": "string",
      "cost": "string",
      "currency": "string"
    }
  ],
  "tools": [
    {}
  ],
  "mcp": [
    {}
  ],
  "provider_errors": [
    {}
  ],
  "artifacts": [
    {
      "id": null,
      "type": "string",
      "status": "string",
      "size_bytes": "string",
      "duration_seconds": "string",
      "checksum_sha256": "string",
      "stored": true
    }
  ]
}

End an in-progress call through the state machine and return the resulting call summary ​

POST/calls/{call}/end
  • Requires call.end.
  • Optional reason body field; an unrecognised value silently falls back to api_request. An already-ended call returns 200 with a message key

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X POST "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/end" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/end`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/end');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.post(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/end",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200JSON object

Errors

StatusWhen it happens
403The key or user lacks the call.end scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": {
    "id": null,
    "status": "string",
    "direction": "string",
    "transport": "string",
    "from": "+40712345678",
    "to": "+40712345678",
    "assistant_id": "01JQ0000000000000000000000",
    "customer_id": "01JQ0000000000000000000000",
    "queue_id": "01JQ0000000000000000000000",
    "operator_id": "01JQ0000000000000000000000",
    "provider": "string",
    "started_at": "2026-01-01T12:00:00+00:00",
    "connected_at": "2026-01-01T12:00:00+00:00",
    "ended_at": "2026-01-01T12:00:00+00:00",
    "duration_seconds": 30,
    "billable_seconds": 30,
    "ended_reason": "string",
    "cost_total": "string",
    "cost_currency": "string",
    "latency": {
      "stt_ms": 1,
      "llm_ttft_ms": 1,
      "tts_ttfa_ms": 1,
      "e2e_ms": 1
    },
    "created_at": "2026-01-01T12:00:00+00:00"
  }
}

List a call's lifecycle events, optionally filtered by type or restricted to errors ​

GET/calls/{call}/events
  • Requires call.read.
  • Query params: type (repeatable), errors_only, limit (default 2000, no upper bound enforced). Not paginated

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/events" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/events`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/events');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/events",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Errors

StatusWhen it happens
403The key or user lacks the call.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": [
    {
      "id": null,
      "type": "string",
      "category": "string",
      "severity": "string",
      "at": "2026-01-01T12:00:00+00:00",
      "offset_ms": "string",
      "sequence": "string",
      "leg_id": "01JQ0000000000000000000000",
      "generation_id": "01JQ0000000000000000000000",
      "payload": "string"
    }
  ]
}

List the conversation turns of a call with role, timing, tool calls and transcription confidence ​

GET/calls/{call}/messages
  • Requires transcript.read.
  • Requires transcript.read, not call.read, and returns every message unpaginated

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/messages" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/messages`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/messages');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/messages",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Errors

StatusWhen it happens
403The key or user lacks the transcript.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": [
    {
      "id": null,
      "role": "string",
      "sequence": "string",
      "turn_index": "string",
      "offset_ms": "string",
      "duration_ms": "string",
      "content": "string",
      "tool_name": "Support Line",
      "tool_arguments": "string",
      "tool_result": "string",
      "interrupted": "string",
      "is_final": "string",
      "language": "string",
      "confidence": "string"
    }
  ]
}

List a call's per-turn latency metrics alongside the aggregate STT, LLM, TTS and end-to-end summary ​

GET/calls/{call}/metrics
  • Requires call.read.
  • The body carries a top-level summary key beside data; a client reading only data misses the aggregate latencies

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/metrics" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/metrics`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/metrics');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/metrics",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Errors

StatusWhen it happens
403The key or user lacks the call.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": [
    {
      "name": "Support Line",
      "component": "string",
      "turn_index": 1,
      "value_ms": 1,
      "value": 1,
      "unit": "string",
      "provider": "string",
      "model": "string",
      "offset_ms": 1,
      "exceeds_target": true
    }
  ],
  "summary": {
    "stt_ms": 1,
    "llm_ttft_ms": 1,
    "tts_ttfa_ms": 1,
    "e2e_ms": 1,
    "e2e_max_ms": 1,
    "turns": 1,
    "interruptions": 1
  }
}

Return a short-lived signed URL for a call's best available recording plus its duration, channels and MIME type ​

GET/calls/{call}/recording
  • Requires recording.read.
  • Picks one file by preference (recording_full, then stereo, then mono); URL lifetime is callagent.storage.signed_url_ttl_minutes, default 15

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/recording" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/recording`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/recording');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/recording",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped object

Errors

StatusWhen it happens
403The key or user lacks the recording.read scope
404No such call, or the call has no ready recording artifact

Example response, generated from the schema:

json
{
  "data": {
    "artifact_id": null,
    "type": "string",
    "url": "https://example.com/hook",
    "expires_at": "2026-01-01T12:00:00+00:00",
    "duration_seconds": 30,
    "channels": 1,
    "mime_type": "string"
  }
}

Purge a call's audio artifacts, leaving the artifact rows as evidence they existed ​

DELETE/calls/{call}/recording
  • Requires recording.delete.
  • This response is not data-wrapped.
  • Body is a bare {"deleted": <int>} where the value is a COUNT of purged artifacts, not a boolean; 0 means nothing was there to purge

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X DELETE "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/recording" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/recording`, {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/recording');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.delete(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/recording",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200Bare JSON object, not data-wrapped

Errors

StatusWhen it happens
403The key or user lacks the recording.delete scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "deleted": 1
}

Return a call's full transcript text with flags saying whether it was redacted or purged ​

GET/calls/{call}/transcript
  • Requires transcript.read.
  • text can be null while the response is still 200: check the purged and redacted flags rather than assuming content

Path parameters

NameTypeRequiredNotes
callstringyes
bash
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/transcript" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/01JQ0000000000000000000000/transcript`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/01JQ0000000000000000000000/transcript');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/01JQ0000000000000000000000/transcript",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped object

Errors

StatusWhen it happens
403The key or user lacks the transcript.read scope
404No call with that id in this organization

Example response, generated from the schema:

json
{
  "data": {
    "call_id": null,
    "text": "string",
    "redacted": true,
    "purged": true
  }
}
GET/calls/search
  • Requires call.read.
  • Routed to the identical CallController@index handler as GET /calls; there is no search-specific behaviour
bash
curl -X GET "$CALLAGENT_API_URL/calls/search" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/calls/search`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/calls/search');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/calls/search",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Errors

StatusWhen it happens
403The key or user lacks the call.read scope

Example response, generated from the schema:

json
{
  "data": [
    {
      "id": null,
      "status": "string",
      "direction": "string",
      "transport": "string",
      "from": "+40712345678",
      "to": "+40712345678",
      "assistant_id": "01JQ0000000000000000000000",
      "customer_id": "01JQ0000000000000000000000",
      "queue_id": "01JQ0000000000000000000000",
      "operator_id": "01JQ0000000000000000000000",
      "provider": "string",
      "started_at": "2026-01-01T12:00:00+00:00",
      "connected_at": "2026-01-01T12:00:00+00:00",
      "ended_at": "2026-01-01T12:00:00+00:00",
      "duration_seconds": 30,
      "billable_seconds": 30,
      "ended_reason": "string",
      "cost_total": "string",
      "cost_currency": "string",
      "latency": {},
      "created_at": "2026-01-01T12:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 1,
    "last_page": 1
  }
}

Documented against the code. Anything not yet implemented is labelled, never described as working.