Skip to main content

Errors

Verified against backend/internal/publicapi/errors.go and webhook_error_class.go.

The error envelope

Every non-2xx response from /api/v1/ext/* has the same shape — an error object, not a flat string. This deliberately matches the convention OpenAI and Anthropic use, because that's the shape a generated SDK and a model writing client code both reach for first:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "phone_number_id is required",
    "hint": "",
    "fields": [
      { "field": "phone_number_id", "reason": "is required" }
    ]
  }
}
FieldMeaning
codeMachine-readable, stable — switch on this
messageHuman-readable, may be reworded over time — don't switch on this
hintWhat to do next, when there's something useful to say
fieldsPresent only for validation failures. Names the JSON field you sent (phone_number_id), never an internal Go field or struct name
detailsPresent on some errors (e.g. INSUFFICIENT_SCOPE includes your_scopes)

Field names are yours, not ours

Validation errors name the field the way you sent it. Earlier versions of this API returned the Go validator's raw output:

text
Key: 'BindPhoneNumberRequest.PhoneNumberID' Error:Field validation for 'PhoneNumberID' failed on the 'required' tag

— a Go type name, Go field name, and struct-tag name, handed to a caller who sent JSON and has never seen any of those three symbols. That's fixed: every field error is now translated back to the JSON key you actually used.

Error codes — the complete set

Every code the public surface can return: 53, read out of the handlers and middleware behind /api/v1/ext on origin/main (2026-08-25). code is stable; message and hint may be reworded. Where one code is used with two statuses, both are listed. An earlier version of this page listed seven and called the set open — it is closed, and this is it.

Authentication and scopes (middleware/api_key_auth.go)

CodeStatusMeaning
MISSING_API_KEY401No X-API-Key and no Authorization: Bearer header
INVALID_API_KEY_FORMAT401The value doesn't look like a key (vm_live_ + 32 characters)
INVALID_API_KEY401Well-formed, but matches no key on record
API_KEY_REVOKED401The key was revoked in the dashboard
API_KEY_EXPIRED401Past its expires_at
NOT_AUTHENTICATED401A scoped route was reached with no authenticated key. You should never see this on /ext; if you do, report it
INSUFFICIENT_SCOPE403Valid key, missing the scope the route requires — details.required_scope names it, details.your_scopes lists what the key has. Scopes are fixed at creation: mint a new key, you can't widen this one

Versioning (middleware/api_version.go)

CodeStatusMeaning
INVALID_API_VERSION400VoiceMatrix-Version isn't a date
UNSUPPORTED_API_VERSION400A date this server doesn't serve — refused rather than silently replaced

Rate limits and quotas

CodeStatusMeaning
RATE_LIMIT_EXCEEDED429Over your tier's request rate — honour Retry-After / X-RateLimit-Reset
RATE_LIMIT_UNAVAILABLE503The limiter's own counter store is unreachable — fails closed rather than letting requests through unchecked
DAILY_CALL_LIMIT_EXCEEDED429POST /calls past the key's daily_call_limit (50 by default)
QUOTA_UNAVAILABLE503The outbound-minutes quota store couldn't answer — if spend can't be accounted for, it isn't spent

Idempotency and request body (middleware/idempotency.go)

CodeStatusMeaning
BODY_TOO_LARGE413Request body over the size cap
IDEMPOTENCY_KEY_IN_FLIGHT409A request with this Idempotency-Key is still being processed — wait, then retry
IDEMPOTENCY_KEY_REUSED422Same Idempotency-Key, different body — a key is bound to the first body it saw
IDEMPOTENCY_UNAVAILABLE503The idempotency store couldn't take the claim — fails closed

Generic

CodeStatusMeaning
VALIDATION_ERROR400A field failed validation — fields[] names each one in your JSON vocabulary
INVALID_REQUEST400 · 422Body isn't valid JSON or couldn't be read (400); an explicit empty prompt on an agent write (422)
NOT_FOUND404No such resource in your organization
ORGANIZATION_REQUIRED403The key is reseller-scoped, with no single organization to write into — a write would have to guess the tenant, so it refuses
INTERNAL_ERROR · DB_ERROR · CREATE_FAILED · UPDATE_FAILED500Ours. Retry with the same Idempotency-Key; if it repeats, [email protected] with the timestamp and path

Agents (publicapi/agents_handler.go, agents_write_handler.go)

CodeStatusMeaning
INVALID_STATUS400Unknown status filter on GET /agents
INVALID_MODEL400model not one of azure-realtime, gemini-realtime, openai-direct
INVALID_WEBHOOK_CONFIG400The agent's pre-call enrichment webhook block failed validation
INVALID_IF_MATCH400If-Match must be the integer agent version
VERSION_CONFLICT412 · 409If-Match doesn't match the current version (412); or the row changed between read and write (409). Re-read, then retry
NO_CLIENT_FOR_ORGANIZATION409The organization has no client record, so an agent can't be created under it
DEFAULT_CLIENT_UNUSABLE409The organization's default client record is not usable — needs a data fix by support; retrying won't help
AGENT_NOT_PUBLISHABLE422POST /agents/:id/publish on an agent with no usable prompt

Calls and leads (calls_handler.go, leads_handler.go)

CodeStatusMeaning
INVALID_PHONE400to isn't a valid phone number
AGENT_NOT_FOUND400agent_id isn't an agent you can use — a 400, not a 404, because the request is what's wrong
CALL_FAILED500The call couldn't be created on the telephony side

Knowledge (knowledge_handler.go)

CodeStatusMeaning
KNOWLEDGE_UNAVAILABLE503Ingestion is off because the embedding service isn't configured on this deployment
KNOWLEDGE_SOURCE_EXISTS409A source with this title already exists on the agent — delete it first, or pick another title
KNOWLEDGE_REJECTED422The content guard refused the document (prompt-injection patterns, English or Hebrew)

Phone numbers (phonenumbers_handler.go, phonenumbers_acquire*.go)

CodeStatusMeaning
ACQUISITION_DISABLED503Self-serve acquisition is switched off on this deployment
SUBSCRIPTION_REQUIRED402Acquisition needs a paid plan
COUNTRY_NOT_SUPPORTED400No self-serve inventory for that country
ORG_NUMBER_LIMIT_REACHED409The organization already holds its included number
PLATFORM_NUMBER_CAPACITY_REACHED402The platform's self-serve pool is exhausted
NO_NUMBERS_AVAILABLE503Pool empty and the provider returned nothing — contact support; the failure is ours
PROVIDER_ERROR · PROVIDER_PURCHASE_FAILED502The carrier refused or failed the purchase
PHONE_NUMBER_ALREADY_BOUND409Bound to another agent — unbind it first

Webhooks (webhooks_handler.go)

CodeStatusMeaning
INVALID_EVENT400An events entry isn't in the 5-event catalog
INVALID_WEBHOOK_URL400The url failed the SSRF guard (private/internal address) or isn't a URL
INVALID_AGENT400agent_id isn't an agent in your organization

Call control (callcontrol_handler.go)

CodeStatusMeaning
TEARDOWN_UNAVAILABLE503Hangup requested but the media layer (LiveKit) isn't configured on this deployment
TEARDOWN_FAILED502The media layer refused the hangup — the response's teardown_ok is false

A known rough edge

A 404 (route or resource not found — e.g. a typo'd webhook URL, or an ID that doesn't exist) currently classifies the same way as a 400/422 (bad payload) in the webhook delivery error-class system described below. That means a customer with a typo in their webhook destination URL gets pointed at their payload, not their URL, when a delivery bounces. Stated here because it's real and unresolved, not because it's been fixed.

Webhook delivery error classes

Separately from the request/response error envelope above, every webhook delivery attempt that doesn't succeed is classified into one of these, visible on the delivery-log entry (GET /webhooks/:id/deliveries). The classes exist because "failed" isn't diagnosable, and four very different situations were all landing in one bucket before this existed.

Ours — something on VoiceMatrix's side of the wire:

ClassMeaning
ssrf_blockedOur own outbound-dial guard refused the resolved destination address
dnsThe destination hostname doesn't resolve
connectTCP refused / reset / unreachable
tlsHandshake or certificate failure
timeoutNo response within the delivery timeout
redirect_blockedA redirect hop failed re-validation
configThe webhook itself is unusable (disallowed HTTP method, bad content type)

Theirs — the destination answered, and said no:

ClassHTTP statusMeaning
auth401Destination rejected our credentials
authz403Destination rejected the request as forbidden
validation400, 404, 422Destination rejected the payload (or — see the rough edge above — the URL doesn't exist)
conflict409Destination says "already exists" — this is usually the integration working correctly, not failing. In one production measurement this was the single largest class (46 of 119 non-success deliveries), and every one was a destination correctly refusing a duplicate registration
rate_limit408, 429Destination asked us to slow down
upstream_4xx / upstream_5xxother 4xx/5xxUncategorized client/server error from the destination

internal covers anything that doesn't match a known pattern — an honest "we don't know" rather than silently absorbing it into an unrelated bucket.

Why this distinction matters in practice: a real production incident had 43 deliveries fail with no response at all. The customer-facing story would have been "your endpoint is broken" — the actual cause was VoiceMatrix's own reachability guard declining to dial the address. Those 43 counted toward the auto-pause threshold and left two customer webhooks paused with the wrong diagnosis attached. The class field is what lets the UI (and you) tell "your server is down" apart from "we wouldn't call your server."

Next

  • Webhooks — delivery, retries, replay, and where these classes surface
  • Authentication — scopes and the requests that produce INSUFFICIENT_SCOPE
All pages