Skip to main content

Endpoint Reference

All endpoints below live under https://voice.vokaai.com/api/v1.

Headers required on every authenticated request:

Authorization: Bearer voka_live_...
Content-Type: application/json (only on POST)

Headers returned on every response:

HeaderPurpose
X-Request-IdUUID — share this with support when filing issues
X-RateLimit-LimitPer-customer ceiling (currently 60/min)
X-RateLimit-RemainingRemaining requests in this window
Retry-AfterSeconds to wait — sent on 429 only

GET /auth/whoami​

Connection-test endpoint. No scope required — any valid key works.

curl https://voice.vokaai.com/api/v1/auth/whoami \
-H "Authorization: Bearer voka_live_..."
{
"customer_id": "0bf91...",
"customer_name": "Acme Dental",
"mode": "live",
"permissions": { "read_calls": true, "manage_webhooks": true }
}

GET /assistants​

List voice assistants. Scope: read_assistants.

Query params: id (specific assistant), limit (default 50, max 100), offset.

curl https://voice.vokaai.com/api/v1/assistants \
-H "Authorization: Bearer voka_live_..."
{
"data": [
{
"id": "550e8400-...",
"name": "Reception Agent",
"description": "Handles new-patient calls",
"language": "en-US",
"voice_id": "natural-female",
"is_active": true,
"phone_numbers": ["+12025551111"],
"created_at": "2026-01-15T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 1 }
}

GET /calls​

Unified list of inbound and outbound calls. Scope: read_calls.

ParamTypeDescription
directioninbound | outboundFilter by direction
assistant_iduuidFilter by assistant
fromISO-8601Inclusive lower bound on started_at
toISO-8601Inclusive upper bound on started_at
limitint 1-100 (default 25)Page size
cursorstringKeyset pagination — pass next_cursor from prior response
pageint ≥ 0Zapier-compat offset alias
curl "https://voice.vokaai.com/api/v1/calls?direction=inbound&limit=10" \
-H "Authorization: Bearer voka_live_..."
{
"data": [
{
"id": "550e8400-...",
"direction": "inbound",
"status": "completed",
"started_at": "2026-05-10T12:00:00Z",
"answered_at": "2026-05-10T12:00:05Z",
"ended_at": "2026-05-10T12:03:30Z",
"duration_seconds": 205,
"from_number": "+12025551111",
"to_number": "+12025552222",
"assistant_id": "550e8400-...",
"assistant_name": "Reception Agent",
"caller_name": "John Doe",
"external_id": "appt_12345",
"has_transcript": true,
"recording_available": true,
"created_at": "2026-05-10T12:00:00Z"
}
],
"next_cursor": "MjAyNi0wNS0xMFQxMjowMDowMC4wMDBafDU1MGU4NDAw..."
}

next_cursor is null when you've reached the end. Pass it as ?cursor=... to fetch the next page. Cursor pagination is recommended for stable iteration; ?page=N is a fallback for tools that can't handle opaque cursors.

external_id is your own reference for the call, passed to POST /outbound-calls. It's present only on API-initiated outbound calls (null for inbound calls and any outbound call placed without one). It's the recommended way to tie a call — and its duration_seconds — back to your customer for usage or chargeback reporting. Filter or group on it in your own system after listing.


GET /calls/{id}​

Single-call detail. Scope: read_calls.

Same fields as the list shape plus: summary, disconnect_reason, conversation_id, cost, billable_seconds.

Returns 404 (not 403) when the call belongs to another customer — we never leak existence.


GET /calls/{id}/transcript​

On-demand transcript pull. Scope: read_calls.

  • Cached transcript: returned immediately (sub-100ms)
  • Not yet cached, conversation_id present: fetched from carrier, persisted, returned
  • Conversation still settling: 202 with status: "pending" — subscribe to call.transcript.ready for push delivery
{
"call_id": "550e8400-...",
"conversation_id": "conv_abc123",
"transcript": {
"full_text": "Caller: hi...\n\nAssistant: ...",
"segments": [
{ "speaker": "caller", "text": "hi", "timestamp": "2026-05-10T12:00:00Z" }
]
}
}

POST /outbound-calls​

Have one of your assistants place a phone call. Scope: outbound_calls.

curl https://voice.vokaai.com/api/v1/outbound-calls \
-H "Authorization: Bearer voka_live_..." \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "550e8400-...",
"to": "+12025551111",
"variables": { "name": "John Doe", "appointment_datetime": "Tuesday at 3pm" },
"external_id": "appt_12345",
"callback_url": "https://your-app.example.com/voka/callback",
"metadata": { "your_internal_id": "abc-123" }
}'
FieldRequiredDescription
assistant_idyesOne of your active assistants
toyesNumber to call, E.164 format (e.g. +12025551111)
fromnoE.164 number to call from. Must be one of your active Voka numbers. Defaults to the first number assigned to the assistant
scheduled_atnoISO-8601 time in the future. Omit to call right away
variablesnoKey/value pairs your assistant's prompt can use (e.g. {{name}})
external_idnoYour own reference for the call. Also works as an idempotency key: a second request with an external_id that is already in use (and not failed or canceled) returns 409
metadatanoYour own tracking data, stored with the call
callback_urlnoURL that receives the completion event for this call (see below)
notification_emailnoAddress that gets an email summary when the call ends

Response 201 Created:

{
"id": "7c9e6679-...",
"external_id": "appt_12345",
"assistant_id": "550e8400-...",
"assistant_name": "Reception Agent",
"from": "+12025559999",
"to": "+12025551111",
"scheduled_at": "2026-05-10T12:00:05Z",
"status": "scheduled",
"variables": { "name": "John Doe", "appointment_datetime": "Tuesday at 3pm" },
"telnyx_event_id": "...",
"created_at": "2026-05-10T12:00:00Z"
}

Status lifecycle: scheduled → calling → completed or failed. A call that never connects (zero duration, never answered) ends as failed. There is no voicemail detection and no automatic retry, so a call that reaches voicemail is handled like any other answered call.

Rate limit and cost protection:

  • 60 requests per minute per customer.
  • Every API key has a daily voice spend cap (default $50, resets at UTC midnight) and a destination country allowlist (default US and Canada). Calls outside the allowlist return 400 with code: "country_not_allowed"; calls past the cap return 429 with code: "daily_limit_exceeded". Both are checked before anything reaches the carrier. To change either one, email support@vokaai.com.

Account requirements:

  • Your account needs a live free trial or an active subscription. Otherwise the request returns 402 with code: "CALLS_REQUIRE_SUBSCRIPTION".
  • On a free trial, calls can be placed right away or scheduled up to 15 minutes ahead (and not past the trial's end). Further out returns 402 with code: "SCHEDULE_BEYOND_TRIAL". Trial calls use trial minutes.
  • Outbound calls use your plan's minutes the same way inbound calls do.

Other errors: 400 for a missing or badly formatted field or a scheduled_at in the past, 403 if the key lacks the outbound_calls scope or from is not one of your numbers, 404 if the assistant isn't yours or isn't active, 502 if the carrier rejects the call (the call is kept with status failed so you can see it in GET /outbound-calls). Errors from this endpoint use the shape { "error": "...", "code": "..." }.

Consent is your responsibility

You are responsible for having the consent required to call each number (for example under the TCPA in the US) and for calling at appropriate times.

Completion callback​

If you set callback_url, Voka sends one POST to it when the call ends:

{
"event": "outbound_call.completed",
"delivered_at": "2026-05-10T12:04:01Z",
"call": {
"id": "7c9e6679-...",
"external_id": "appt_12345",
"status": "completed",
"started_at": "2026-05-10T12:00:05Z",
"answered_at": "2026-05-10T12:00:11Z",
"ended_at": "2026-05-10T12:03:59Z",
"duration_seconds": 228,
"cost": 0.46,
"recording_url": "https://...",
"conversation_id": "...",
"metadata": { "your_internal_id": "abc-123" },
"to_number": "+12025551111",
"from_number": "+12025559999",
"assistant_id": "550e8400-..."
}
}
  • The event is named outbound_call.completed for every finished call; check call.status (completed or failed).
  • It is a single attempt with an 8-second timeout. There are no retries. If you miss it, use GET /outbound-calls to check the result.
  • It is not signed. Unlike webhook subscriptions, there is no HMAC header. Use a hard-to-guess URL (for example one with a secret token in the path) and treat the payload as a hint to re-fetch.

GET /outbound-calls​

List the outbound calls placed through the API. Scope: read_analytics.

ParamDescription
statusscheduled, calling, completed, failed or canceled
assistant_idFilter by assistant
external_idFilter by your reference
limitDefault 50, max 100
offsetPagination offset
curl "https://voice.vokaai.com/api/v1/outbound-calls?external_id=appt_12345" \
-H "Authorization: Bearer voka_live_..."
{
"data": [
{
"id": "7c9e6679-...",
"external_id": "appt_12345",
"assistant_id": "550e8400-...",
"from_number": "+12025559999",
"to_number": "+12025551111",
"status": "completed",
"scheduled_at": "2026-05-10T12:00:05Z",
"duration_seconds": 228,
"created_at": "2026-05-10T12:00:00Z"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 1 }
}

List rows use the stored column names (from_number, to_number) and include more fields than shown here. This endpoint uses limit/offset only (no cursor). There is no single-call GET /outbound-calls/{id}; filter the list by external_id, or find the call in GET /calls (direction=outbound), which also returns external_id.


GET /outbound-calls/{id}/transcript​

Turn-by-turn transcript for an API-placed outbound call. Scope: read_analytics.

{
"outbound_call_id": "7c9e6679-...",
"conversation_id": "...",
"status": "completed",
"transcript": {
"full_text": "Assistant: Hi, this is...\n\nCaller: ...",
"segments": [
{ "speaker": "assistant", "text": "Hi, this is...", "timestamp": "2026-05-10T12:00:11Z" }
]
}
}

Until the call has ended and its conversation is saved, this returns 404 with "error": "Transcript not available yet", the current status, and a hint. Retry once the status is completed.


POST /webhook-subscriptions​

Subscribe to one or more events. Scope: manage_webhooks.

curl https://voice.vokaai.com/api/v1/webhook-subscriptions \
-H "Authorization: Bearer voka_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/voka",
"events": ["call.completed", "call.transcript.ready"],
"description": "Production receiver"
}'
{
"id": "sub_01H...",
"url": "https://your-app.example.com/webhooks/voka",
"events": ["call.completed", "call.transcript.ready"],
"secret": "whsec_...",
"is_active": true,
"created_at": "2026-05-10T12:00:00Z"
}
The secret is shown once

Store it now — used to verify HMAC signatures on incoming events. We can rotate it but cannot show it again.

Idempotent: posting the same (url, events) set returns the existing subscription with 200 (no new secret) instead of creating a duplicate.

Make.com verification probe: if you POST with ?challenge=<token> query param, we echo { challenge: token } with 202 Accepted — no auth, no DB write. This is the contract Make's connection-validation step expects.

GET /webhook-subscriptions​

List your active subscriptions. Scope: manage_webhooks.

DELETE /webhook-subscriptions/{id}​

Unsubscribe. Scope: manage_webhooks. Also fired automatically when a subscription has 10 consecutive delivery failures (auto-disable, not delete — re-enable via the dashboard).


Pagination conventions​

GET /calls supports both pagination styles so wrapper authors can pick (GET /assistants and GET /outbound-calls use limit/offset):

Cursor (recommended):

curl "https://voice.vokaai.com/api/v1/calls?limit=25"
# Response includes: "next_cursor": "..."
curl "https://voice.vokaai.com/api/v1/calls?limit=25&cursor=..."

Offset (Zapier-friendly):

curl "https://voice.vokaai.com/api/v1/calls?limit=25&page=0"
curl "https://voice.vokaai.com/api/v1/calls?limit=25&page=1"

When both cursor and page are passed, cursor wins. Cursor is O(1) per page; page is O(N) for deep pages so prefer cursor for sync jobs.