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 | /calls | List the organization's calls with filter, sort and pagination, returning a summary row per call plus page meta |
| POST | /calls | Place 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}/artifacts | List the stored artifacts for a call with type, status, size, checksum and purge state |
| GET | /calls/{call}/artifacts/{artifact}/download | Signed fallback download used when the storage driver cannot sign a URL itself |
| GET | /calls/{call}/costs | Break a call's cost down by component and provider, alongside the per-category totals stored on the call |
| GET | /calls/{call}/debug | Export a sanitized diagnostic bundle for a call with the assistant snapshot, legs, events, metrics and costs |
| POST | /calls/{call}/end | End an in-progress call through the state machine and return the resulting call summary |
| GET | /calls/{call}/events | List a call's lifecycle events, optionally filtered by type or restricted to errors |
| GET | /calls/{call}/messages | List the conversation turns of a call with role, timing, tool calls and transcription confidence |
| GET | /calls/{call}/metrics | List a call's per-turn latency metrics alongside the aggregate STT, LLM, TTS and end-to-end summary |
| GET | /calls/{call}/recording | Return a short-lived signed URL for a call's best available recording plus its duration, channels and MIME type |
| DELETE | /calls/{call}/recording | Purge a call's audio artifacts, leaving the artifact rows as evidence they existed |
| GET | /calls/{call}/transcript | Return a call's full transcript text with flags saying whether it was redacted or purged |
| GET | /calls/search | Search 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
- Requires
call.read. - Only whitelisted filters are read; unknown query params and unknown sort columns are ignored silently rather than rejected
curl -X GET "$CALLAGENT_API_URL/calls" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped array |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
Example response, generated from the schema:
{
"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
- Requires
call.create. - Accepts an
Idempotency-Keyheader; 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
| Field | Type | Required | Notes |
|---|---|---|---|
to | string | yes | max 128 chars |
from | string | null | — | max 128 chars |
assistant_id | string | null | — | |
assistant_version_id | string | null | — | |
assistant_overrides | string[] | null | — | |
squad_id | string | null | — | |
phone_number_id | string | null | — | |
sip_trunk_id | string | null | — | |
customer_id | string | null | — | |
campaign_id | string | null | — | |
transport | pstn · sip · web · mobile · null | null | — | |
provider | string | null | — | max 32 chars |
variables | string[] | null | — | |
metadata | string[] | null | — | |
idempotency_key | string | null | — | max 128 chars |
answering_machine_detection | boolean | 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_seconds | integer | null | — | min 5. max 300 |
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"
}'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
$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'];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
| Status | Body |
|---|---|
201 | data-wrapped object |
Errors
| Status | When it happens |
|---|---|
403 | Missing call.create scope, or the API key is restricted to other assistants |
422 | The payload failed validation (to is required; ids must be ULIDs) |
Example response, generated from the schema:
{
"data": {}
}Fetch one call in full, including legs, recording state, metadata, variables and analysis fields
- Requires
call.read.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped object |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"data": {}
}Delete a call record and return the deleted id
- 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
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X DELETE "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | Bare JSON object, not data-wrapped |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.delete scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"deleted": true,
"id": null
}List the stored artifacts for a call with type, status, size, checksum and purge state
- Requires
call.read. - Returns metadata only; no download URL is included. Use GET /calls/{call}/recording or a signed download URL
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/artifacts" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped array |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"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
- 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
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes | |
artifact | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/artifacts/01JQ0000000000000000000000/download" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | Not JSON — text/html |
Errors
| Status | When it happens |
|---|---|
403 | The URL signature is missing, altered or expired |
404 | Unknown 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
- Requires
call.read. - The body carries a top-level
summarykey besidedata; a client reading onlydatamisses the totals
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/costs" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped array |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"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
- 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
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/debug" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | Bare JSON object, not data-wrapped |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"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
- Requires
call.end. - Optional
reasonbody field; an unrecognised value silently falls back to api_request. An already-ended call returns 200 with amessagekey
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X POST "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/end" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | JSON object |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.end scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"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
- Requires
call.read. - Query params: type (repeatable), errors_only, limit (default 2000, no upper bound enforced). Not paginated
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/events" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped array |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"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
- Requires
transcript.read. - Requires transcript.read, not call.read, and returns every message unpaginated
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/messages" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped array |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the transcript.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"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
- Requires
call.read. - The body carries a top-level
summarykey besidedata; a client reading onlydatamisses the aggregate latencies
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/metrics" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped array |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"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
- 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
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/recording" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped object |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the recording.read scope |
404 | No such call, or the call has no ready recording artifact |
Example response, generated from the schema:
{
"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
- 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
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X DELETE "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/recording" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | Bare JSON object, not data-wrapped |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the recording.delete scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"deleted": 1
}Return a call's full transcript text with flags saying whether it was redacted or purged
- Requires
transcript.read. textcan be null while the response is still 200: check thepurgedandredactedflags rather than assuming content
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
call | string | yes |
curl -X GET "$CALLAGENT_API_URL/calls/01JQ0000000000000000000000/transcript" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped object |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the transcript.read scope |
404 | No call with that id in this organization |
Example response, generated from the schema:
{
"data": {
"call_id": null,
"text": "string",
"redacted": true,
"purged": true
}
}Search calls using the same filters, sorting and pagination as GET /calls
- Requires
call.read. - Routed to the identical CallController@index handler as GET /calls; there is no search-specific behaviour
curl -X GET "$CALLAGENT_API_URL/calls/search" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Accept: application/json"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
$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'];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
| Status | Body |
|---|---|
200 | data-wrapped array |
Errors
| Status | When it happens |
|---|---|
403 | The key or user lacks the call.read scope |
Example response, generated from the schema:
{
"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
}
}