Concepts
Errors
Voximate answers with standard HTTP status codes, and every error body has the same shape — so one error handler covers the whole API.
The error shape
Response · 400
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "`to` must be a phone number in E.164 format, for example +4712345678.",
"param": "to"
},
"request_id": "req_8f14e45fceea167a"
}type— the broad category. Branch on this first.code— a stable, specific reason. Safe to branch on; codes are never renamed within v1.message— a sentence for people. Log it, show it to a developer, but do not parse it: wording may improve.param— the field the error is about, ornull.request_id— the same value as theX-Request-Idheader. Log it, and quote it to support.
Error types
| type | HTTP status | Meaning |
|---|---|---|
invalid_request_error | 400, 422 | The request is malformed (400), or valid but impossible right now (422). |
authentication_error | 401 | No key, or the key is invalid, expired or revoked. |
permission_error | 403 | The key lacks the scope or role this needs. |
not_found_error | 404 | No such resource in your workspace. |
idempotency_error | 409 | An Idempotency-Key clash. |
rate_limit_error | 429 | Too many requests, too many calls, or the plan quota is used up. |
api_error | 5xx | Something went wrong on our side. Safe to retry with backoff. |
Error codes
The codes you are most likely to meet. New codes may be added; treat an unknown one by its type.
| code | Status | What to do |
|---|---|---|
parameter_missing | 400 | A required field or parameter is missing. param names it. |
parameter_invalid | 400 | A field has the wrong type or format. param names it. |
parameter_unknown | 400 | A field this endpoint does not take. Unknown fields are refused, never ignored. param names it. |
body_invalid | 400 | The body is not valid JSON, or not a JSON object. |
body_empty | 400 | A PATCH with nothing to change. Send at least one field. |
body_too_large | 400 | The request body is too large. |
content_type_invalid | 400 | Send bodies as Content-Type: application/json. |
request_invalid | 400 | The request could not be read. |
api_key_missing | 401 | No Authorization: Bearer … header. |
api_key_invalid | 401 | The key is wrong, expired or revoked. |
missing_scope | 403 | The key needs a scope it does not have. The message names it. |
role_required | 403 | Adding, changing, testing or removing a webhook endpoint needs a key made by a workspace owner or admin. |
resource_missing | 404 | Nothing with that id in your workspace. |
route_not_found | 404 | No such endpoint. Check the method and the path. |
idempotency_key_reused | 409 | This Idempotency-Key was used with a different request. |
idempotency_request_in_progress | 409 | The first request with this key is still running. Retry shortly. |
agent_not_published | 422 | The agent must be published before it can place calls. |
agent_not_ready | 422 | The agent cannot be published or unpublished as it is. The message says why. |
agent_has_no_number | 422 | The agent has no phone number to call from. Assign one first. |
from_number_required | 422 | The agent has several numbers. Say which one with from. |
from_number_not_assigned | 422 | from is not one of the numbers assigned to this agent. |
emergency_number | 422 | Emergency numbers can never be called by an agent. |
call_rejected | 422 | The call could not be started (for example the free trial allows one call at a time). The message says why. |
rate_limited | 429 | Too many requests. Wait Retry-After seconds. |
call_rate_limited | 429 | Too many outbound calls in the last minute. |
call_daily_limit | 429 | The daily outbound call limit is reached. |
quota_exceeded | 429 | Your plan’s call quota is used up. |
internal_error | 500 | Something went wrong on our side. Retry; if it keeps happening, contact us with the request id. |
upstream_error | 502 | A Voximate service answered with an error. Retry with backoff. |
upstream_unavailable | 503 | A Voximate service did not answer in time. Retry with backoff. |
auth_unavailable | 503 | The key could not be checked just now. Retry with backoff. |
call_outcome_unknown | 503 | POST /calls only: we could not confirm whether the call started, and it may be ringing. Look for it with GET /calls before trying again with a new Idempotency-Key. |
Handling errors
- Retry only
429and5xx. Every other4xxwill fail the same way again until the request changes. - On
429, wait theRetry-Afterseconds. On5xx, back off exponentially with jitter. - Retry a
POSTonly with anIdempotency-Key, so a retry can never start a second call or create a second agent. - Log
request_idwith every failure.
voximate.js
// Retry 429 and 5xx with exponential backoff; never retry other 4xx.
async function voximate(path, init = {}, attempt = 0) {
const res = await fetch(`https://api.voximate.no/public/v1${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.VOXIMATE_API_KEY}`,
'Content-Type': 'application/json',
...init.headers,
},
});
const retryable = res.status === 429 || res.status >= 500;
if (retryable && attempt < 4) {
const retryAfter = Number(res.headers.get('Retry-After'));
const wait = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(30_000, 500 * 2 ** attempt) + Math.random() * 250;
await new Promise((r) => setTimeout(r, wait));
return voximate(path, init, attempt + 1);
}
const body = await res.json();
if (!res.ok) {
const err = new Error(body.error.message);
Object.assign(err, { type: body.error.type, code: body.error.code, requestId: body.request_id });
throw err;
}
return body;
}