Error Responses
The Voka API uses RFC 9457 problem details (the successor to RFC 7807) for every error response. Every error has the same shape and a stable code field for machine-readable handling.
Envelope
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"detail": "API key does not have read_calls permission.",
"code": "perm.denied",
"request_id": "8c1c1b2a-9f3e-4a4d-9f9b-2c0e0f1a2b3c"
}
| Field | Type | Description |
|---|---|---|
type | URI | Currently always about:blank — the title is the canonical type label |
title | string | Short human-readable summary (HTTP status text) |
status | int | HTTP status code (matches the response status) |
detail | string | Longer human-readable explanation |
code | string | Stable machine identifier — branch on this |
request_id | uuid | Pass to support; also echoed in X-Request-Id response header |
Content-Type is application/problem+json; charset=utf-8 per RFC 9457. Most JSON clients parse it fine; if yours is strict about content type, accept application/*+json.
Code reference
code | Status | Meaning | Action |
|---|---|---|---|
auth.missing | 401 | No Authorization header | Add the header |
auth.invalid | 401 | Token doesn't match voka_live_… format | Check key value |
auth.revoked | 401 | Key was disabled in the dashboard | Re-authenticate |
auth.expired | 401 | Key passed its expires_at | Mint a new key |
perm.denied | 403 | Key lacks the required scope | Add the scope in the dashboard |
rate.exceeded | 429 | Per-customer bucket empty | Honor Retry-After |
rate.exceeded_key | 429 | Per-API-key bucket empty | Honor Retry-After |
input.invalid | 422 | Request input failed validation | See issues[] for field-level details |
resource.not_found | 404 | Resource doesn't exist or belongs to another customer | Check the ID |
resource.conflict | 409 | Conflicts with existing state (rare) | Reload + retry |
carrier.unavailable | 502 | Upstream provider unavailable | Transient — retry with backoff |
internal.error | 500 | Unexpected server-side error | Retry; if persistent, share request_id with support |
Validation errors carry per-field issues
When you send invalid input — bad enum value, malformed UUID, out-of-range number — the response includes an issues array:
curl "https://voice.vokaai.com/api/v1/calls?limit=abc&direction=sideways" \
-H "Authorization: Bearer voka_live_..."
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Request input failed validation.",
"code": "input.invalid",
"request_id": "...",
"issues": [
{ "path": ["limit"], "message": "Expected number, received string" },
{ "path": ["direction"], "message": "Invalid enum value. Expected 'inbound' | 'outbound'" }
]
}
Handling errors in a wrapper
Recommended branching pattern (TypeScript):
async function callVoka(path: string, init: RequestInit) {
const res = await fetch(`https://voice.vokaai.com${path}`, init);
if (res.ok) return res.json();
const problem = await res.json();
switch (problem.code) {
case 'auth.revoked':
case 'auth.expired':
// Prompt user to re-authenticate
throw new ReAuthRequiredError(problem.detail);
case 'rate.exceeded':
case 'rate.exceeded_key':
// Wait and retry
const retryAfter = parseInt(res.headers.get('retry-after') ?? '60');
await sleep(retryAfter * 1000);
return callVoka(path, init);
case 'input.invalid':
// Show field-level errors to the user
throw new ValidationError(problem.issues);
case 'carrier.unavailable':
case 'internal.error':
// Transient — retry with backoff, surface request_id on persistent failure
throw new TransientError(problem.detail, problem.request_id);
default:
throw new UnknownError(problem);
}
}
Filing a bug
Include the request_id from the failing response — it lets us trace the exact request through our logs and audit trails. Without it triage is much slower.
Email: support@vokaai.com.