Calls
Access your complete call history, retrieve transcripts and recordings, poll live call status, and control in-progress calls -- end, transfer, or collect DTMF digits from the caller.
The call object
{
"call_id": "call_00000001",
"agent_id": "agent_28c51f81",
"from_number": "+31612345678",
"to_number": "+31850012345",
"status": "completed",
"disposition": "answered",
"ended_reason": "caller-hangup",
"duration_seconds": 127,
"minutes_used": 2.12,
"rate_per_minute": 0.25,
"total_cost": 0.5300,
"has_recording": false,
"turn_count": 6,
"tool_call_count": 2,
"tools_called": ["get_policy", "create_ticket"],
"metadata": {},
"ws_unavailable": false,
"transcript": [...], // only on GET /v1/calls/:callId
"latency_data": {...}, // only on GET /v1/calls/:callId
"call_cost": 0.4664,
"recording_cost": 0.0,
"llm_discount": 0.0,
"transfer_duration_seconds": 0,
"transfer_cost": 0.0,
"transferred_to": null,
"recording_enabled": false,
"extracted_data": {},
"started_at": "2026-01-15T10:00:00Z",
"ended_at": "2026-01-15T10:02:07Z"
}status values
ringing | in-progress | completed | failed | busy | no-answer | canceled | rejectedended_reason values
agent-hangup | caller-hangup | api | timeout | transfer | silence-timeout | error | max-durationdisposition values
answered | no-answer | transferred | busy | failed | voicemail | rejectedmetadata — arbitrary JSON set by your server_url handler during the call. Read-only via the API.
extracted_data — structured JSON populated post-call by the LLM when an extraction schema is returned from your server_url. Read-only via the API.
latency_data — per-turn latency breakdown, present on GET /v1/calls/:callId only: { turns: [{turn, ts, llm_ttfb_ms, tts_start_ms, tts_ttfb_ms}] }. All values are milliseconds from the start of that turn.
ws_unavailable — true if the voice WebSocket was unreachable during the call (indicates a delivery degradation, not a billing impact).
/v1/calls/summaryAggregate stats for a date range.
| Parameter | Default | Description |
|---|---|---|
| agent_id | — | Filter to a specific agent by numeric id only (agent_* public IDs are not supported here) |
| from_date | — | Start date YYYY-MM-DD (inclusive) |
| to_date | — | End date YYYY-MM-DD (inclusive) |
curl "https://api.staffifyai.com/v1/calls/summary?from_date=2026-01-01&to_date=2026-01-31" \
-H "Authorization: Bearer sfy_live_YOUR_KEY"
# Response
{
"total_calls": 1240,
"completed_calls": 1198,
"failed_calls": 12,
"answered_calls": 1195,
"no_answer_calls": 30,
"total_duration_seconds": 153624,
"total_minutes_used": 2560.4,
"total_cost": 563.29,
"total_recording_cost": 0.0,
"total_llm_discount": 0.0,
"filters": { "from_date": "2026-01-01", "to_date": "2026-01-31" }
}/v1/callsPaginated list of calls. Excludes transcript and latency_data (use GET /v1/calls/:callId for those).
| Parameter | Default | Description |
|---|---|---|
| agent_id | — | Filter by agent (agent_* public_id or numeric id) |
| status | — | Filter by status (ringing, in-progress, completed, failed, busy, no-answer, canceled, rejected) |
| disposition | — | Filter by disposition (answered, no-answer, transferred, busy, failed, voicemail, rejected) |
| from_date | — | Start date YYYY-MM-DD (inclusive) |
| to_date | — | End date YYYY-MM-DD (inclusive) |
| after | — | Cursor from previous response's next_cursor |
| limit | 50 | Results per page (max 100) |
# Answered calls in January curl "https://api.staffifyai.com/v1/calls?disposition=answered&from_date=2026-01-01&to_date=2026-01-31" \ -H "Authorization: Bearer sfy_live_YOUR_KEY" # All calls for one agent today curl "https://api.staffifyai.com/v1/calls?agent_id=agent_28c51f81&from_date=2026-06-03" \ -H "Authorization: Bearer sfy_live_YOUR_KEY"
/v1/calls/:callIdFull call object including transcript and latency_data.
curl "https://api.staffifyai.com/v1/calls/call_00000001" \
-H "Authorization: Bearer sfy_live_YOUR_KEY"
# Transcript format (in response):
"transcript": [
{ "role": "assistant", "content": "Hello, how can I help?", "timestamp": 0.0 },
{ "role": "user", "content": "I need to file a claim", "timestamp": 2.4 }
]/v1/calls/:callId/statusLightweight endpoint for polling a live call's state. Reads from Redis when in-progress -- fast and cheap to poll every few seconds.
curl "https://api.staffifyai.com/v1/calls/call_00000001/status" \
-H "Authorization: Bearer sfy_live_YOUR_KEY"
# Response
{
"call_id": "call_00000001",
"status": "in-progress",
"duration_seconds": 42,
"started_at": "2026-01-15T10:00:00Z",
"ended_at": null
}/v1/calls/:callId/recordingReturns a pre-signed S3 URL. Use ?expires_in to control expiry (60–86400 seconds, default 3600).
curl "https://api.staffifyai.com/v1/calls/call_00000001/recording?expires_in=7200" \
-H "Authorization: Bearer sfy_live_YOUR_KEY"
# Response
{
"url": "https://s3.amazonaws.com/staffify-recordings/...?X-Amz-Expires=7200...",
"expires_at": "2026-06-03T12:00:00Z"
}| HTTP status | Error message | Cause |
|---|---|---|
| 404 | Recording was not enabled for this call | The agent did not have recording turned on |
| 404 | Recording not yet available | Call ended but the recording upload is still processing |
/v1/calls/:callId/endEnd a live call programmatically. Only works when status is in-progress. The resulting call record will have ended_reason: "api".
curl -X POST https://api.staffifyai.com/v1/calls/call_00000001/end \
-H "Authorization: Bearer sfy_live_YOUR_KEY"
# Response
{ "success": true }Returns 409 "Call is not in progress" if the call is already ended.
/v1/calls/:callId/transferTransfer a live call to a phone number. Fires transfer.initiated then transfer.completed or transfer.failed.
| Field | Required | Description |
|---|---|---|
| destination | E.164 phone number to transfer to (e.g. +31850099999) |
curl -X POST https://api.staffifyai.com/v1/calls/call_00000001/transfer \
-H "Authorization: Bearer sfy_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "destination": "+31850099999" }'
# Response
{ "success": true, "destination": "+31850099999" }| HTTP status | Error message | Cause |
|---|---|---|
| 400 | destination must be a valid E.164 phone number | Missing or malformed destination |
| 409 | Call is not in progress | Call is already ended |
| 409 | Call does not have an active control channel | Inbound call was not answered via Telnyx (no call_control_id) |
| 422 | Transfer failed: <reason> | Telnyx rejected the transfer request |
/v1/calls/:callId/gatherPlay a TTS prompt and collect DTMF keypad digits from the caller. Only one gather active per call at a time. Fires call.gather.completed when done.
| Field | Default | Description |
|---|---|---|
| prompt | — | Text to speak (required) |
| max_digits | 10 | 1–20 digits to collect |
| terminator | "#" | Digit that ends input early |
| timeout_seconds | 15 | Seconds to wait for input (5–60) |
| voice | "female" | "female" or "male" |
| language | "en-US" | BCP-47 language for the TTS prompt |
curl -X POST https://api.staffifyai.com/v1/calls/call_00000001/gather \
-H "Authorization: Bearer sfy_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Please enter your 8-digit account number followed by the hash key.",
"max_digits": 8,
"terminator": "#",
"timeout_seconds": 20
}'
# HTTP 202 response
{ "message": "Gather initiated", "call_id": 1 }Returns HTTP 202 (Accepted). The gather runs asynchronously; subscribe to the call.gather.completed webhook to receive the result.
Note: call_id in the 202 response body is the internal numeric database ID (e.g. 1), not the public call_XXXXXXXX identifier. Use the call.gather.completed webhook payload for the public call_id.
call.gather.completed webhook payload
{
"event": "call.gather.completed",
"call_id": "call_00000001",
"agent_id": "agent_28c51f81",
"status": "completed", // "completed" | "timeout"
"input": "12345678" // null when status is "timeout"
}Returns 409 "A gather operation is already in progress for this call" if another gather is active. Returns 409 "Call is not active" if the call is not in-progress.
/v1/calls/:callId/debug-linkGenerate a shareable public debug link for a call. The link can be opened in a browser without an API key. Phone numbers are masked and all conversation turns with per-turn latency metrics (LLM TTFB, TTS start, TTS TTFB) are visible. Useful for sharing call details with support or a third party without exposing raw data.
curl "https://api.staffifyai.com/v1/calls/call_00000001/debug-link" \
-H "Authorization: Bearer sfy_live_YOUR_KEY"
# Response
{
"debug_token": "kR7xQ2mNpLwV...(43-char base64url token)",
"url": "/api/v1/call-debug/kR7xQ2mNpLwV..."
}
# The /api/v1/call-debug/:token endpoint is public -- no API key required
# Prefix with the base URL to get a full link:
# https://api.staffifyai.com/api/v1/call-debug/kR7xQ2mNpLwV...
# Response includes call metadata, masked phone numbers, and turn-by-turn transcript with latency:
{
"call_id": "call_00000001",
"status": "completed",
"duration_seconds": 127,
"from": "+316*****678",
"to": "+318*****345",
"started_at": "2026-01-15T10:00:00Z",
"turns": [
{
"role": "assistant",
"content": "Hello, how can I help?",
"llm_ttfb_ms": 210,
"tts_start_ms": 320,
"tts_ttfb_ms": 95
},
{
"role": "user",
"content": "I need to file a claim"
}
]
}Recording format and retention
Recordings are stored as MP3 and retained for 90 days. Recordings on suspended accounts are retained for 7 days before deletion.
Recording cost: billed per minute of actual call duration. One minute is pre-debited when the call starts; the balance is settled when the call ends. The recording_cost field on the call object reflects the final charge.
Returns 404 "Debug log not available" if the call has no debug token (only calls processed through the full API voice pipeline have one).
/v1/calls/:callId/webhooksReturns up to 500 webhook delivery records for this call across all webhooks. Useful for debugging missed events.
curl "https://api.staffifyai.com/v1/calls/call_00000001/webhooks" \
-H "Authorization: Bearer sfy_live_YOUR_KEY"
# Response
{
"call_id": "call_00000001",
"deliveries": [
{
"id": 1,
"event": "call.started",
"event_id": "a1b2c3d4-...",
"url": "https://your-server.example.com/webhook",
"attempt": 1,
"status_code": 200,
"success": true,
"error": null,
"created_at": "2026-01-15T10:00:01Z"
},
{
"id": 2,
"event": "call.ended",
"event_id": "e5f6a7b8-...",
"url": "https://your-server.example.com/webhook",
"attempt": 1,
"status_code": 503,
"success": false,
"error": "HTTP 503",
"created_at": "2026-01-15T10:02:09Z"
}
]
}Results are ordered by created_at ASC, attempt ASC and capped at 500 records. success is a boolean. error is a string or null.