Skip to main content
API Reference

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 | rejected

ended_reason values

agent-hangup | caller-hangup | api | timeout | transfer | silence-timeout | error | max-duration

disposition values

answered | no-answer | transferred | busy | failed | voicemail | rejected

metadata — 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).

GET/v1/calls/summary

Aggregate stats for a date range.

ParameterDefaultDescription
agent_idFilter to a specific agent by numeric id only (agent_* public IDs are not supported here)
from_dateStart date YYYY-MM-DD (inclusive)
to_dateEnd 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" }
}
GET/v1/calls

Paginated list of calls. Excludes transcript and latency_data (use GET /v1/calls/:callId for those).

ParameterDefaultDescription
agent_idFilter by agent (agent_* public_id or numeric id)
statusFilter by status (ringing, in-progress, completed, failed, busy, no-answer, canceled, rejected)
dispositionFilter by disposition (answered, no-answer, transferred, busy, failed, voicemail, rejected)
from_dateStart date YYYY-MM-DD (inclusive)
to_dateEnd date YYYY-MM-DD (inclusive)
afterCursor from previous response's next_cursor
limit50Results 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"
GET/v1/calls/:callId

Full 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 }
]
GET/v1/calls/:callId/status

Lightweight 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
}
GET/v1/calls/:callId/recording

Returns 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 statusError messageCause
404Recording was not enabled for this callThe agent did not have recording turned on
404Recording not yet availableCall ended but the recording upload is still processing
POST/v1/calls/:callId/end

End 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.

POST/v1/calls/:callId/transfer

Transfer a live call to a phone number. Fires transfer.initiated then transfer.completed or transfer.failed.

FieldRequiredDescription
destinationrequiredE.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 statusError messageCause
400destination must be a valid E.164 phone numberMissing or malformed destination
409Call is not in progressCall is already ended
409Call does not have an active control channelInbound call was not answered via Telnyx (no call_control_id)
422Transfer failed: <reason>Telnyx rejected the transfer request
POST/v1/calls/:callId/gather

Play 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.

FieldDefaultDescription
promptText to speak (required)
max_digits101–20 digits to collect
terminator"#"Digit that ends input early
timeout_seconds15Seconds 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.

GET/v1/calls/:callId/webhooks

Returns 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.

Calls - Staffify API