All voice endpoints sit under https://api.senderz.com/v1 and use the same Authorization: Bearer API key as the messaging endpoints. Every response carries a code field on errors so you can branch without parsing prose.
Voice numbers
POST /v1/voice/numbers
Buy a number and attach it to your voice connection. Returns 503 NOT_CONFIGURED if voice is not provisioned for your account, 404 NOT_AVAILABLE if no numbers in the requested area code, 502 TELNYX_ERROR if the carrier rejects the order.
curl -X POST https://api.senderz.com/v1/voice/numbers \
-H "Authorization: Bearer tf_live_YOUR_KEY" \
-d '{"country":"US","area_code":"619"}'
GET /v1/voice/numbers
Lists every active or released number scoped to your tenant. Foreign-tenant numbers never leak into this list.
PATCH /v1/voice/numbers/:id
Change the assigned persona. assigned_persona_id may be null to detach. The persona must belong to the same tenant or you get 404.
DELETE /v1/voice/numbers/:id
Best-effort releases the number on Telnyx and marks the row released. Idempotent — already-released ids return 200.
Calls
POST /v1/calls
Dial outbound. The AI opens with a brief intro and pursues the supplied script as the goal of the call. Returns 201 with { id, status: 'ringing', to }.
curl -X POST https://api.senderz.com/v1/calls \
-H "Authorization: Bearer tf_live_YOUR_KEY" \
-d '{
"from": "<voice_number_id>",
"to": "+14155551234",
"script": "Confirm Friday 2pm appointment for John Smith."
}'
Errors:
404 NUMBER_NOT_OWNED— thefromvoice number is not yours or is released.451 OPT_OUT— the destination has opted out of messages for this tenant.429 RATE_LIMITED— more than 10 outbound calls in the current minute.429 BLOCKED_DESTINATION— five dial failures to this destination in the last hour triggered a 1-hour block.502 TELNYX_ERROR— carrier rejected the dial.
GET /v1/calls
Filters: direction=inbound|outbound, status, from_date, to_date (YYYY-MM-DD). Pagination via limit (max 100) and offset.
GET /v1/calls/:id
Full call detail including transcript, AI summary, lead score, and has_recording. The R2 key is intentionally omitted — use /recording to play back.
GET /v1/calls/:id/events
Returns the call_events log for a single call, ordered ascending. Useful for debugging which intents fired in what order.
GET /v1/calls/:id/recording
Mints a 15-minute playback URL pointing at /v1/calls/:id/recording/stream. The stream endpoint re-checks tenant ownership and TTL before serving the MP3.
Intents
GET /v1/voice/intents
Returns built-in (talk_to_human, leave_message, end_call) plus custom intents.
POST /v1/voice/intents
Create a custom intent.
curl -X POST https://api.senderz.com/v1/voice/intents \
-H "Authorization: Bearer tf_live_YOUR_KEY" \
-d '{
"name": "book_table",
"description": "Caller wants to book a table — capture party size and time.",
"action": "webhook",
"action_config": { "url": "https://your-app.example.com/voice/book" },
"language": "all"
}'
name must be ^[a-z][a-z0-9_]{1,39}$ and unique per tenant. action is one of transfer, message, webhook, end_call. language is one of en, es-MX, tr-TR, all.
PATCH /v1/voice/intents/:id
Updates description / action / config / language / enabled. name is immutable — delete and recreate to rename.
DELETE /v1/voice/intents/:id
Removes a custom intent. Built-in names that get deleted reseed from the prompt builder’s defaults.
Billing
GET /v1/billing/voice
Current voice subscription state plus minutes used.
POST /v1/billing/voice/subscribe
Body: { "tier": "starter" | "growth" | "scale" }. Requires an active messaging subscription on the tenant.
POST /v1/billing/voice/cancel
Cancels the voice subscription item at period end. Returns 404 NOT_FOUND if voice is not active for this tenant.