Call Lens
Going live

Errors

Every code Call Lens can refuse with, and what a client should do about each.

Every refusal in the standard envelope carries a stable error code. Switch on that code, never on message — the prose is written for humans and will change. (Field-validation failures are the exception: they use a different shape entirely, with no error key at all — see Two different 422 shapes below.)

{ "success": false, "message": "…", "error": "ingest_daily_audio_allowance_exhausted", "data": {  } }

The refusal codes

The status depends on which endpoint you called, because the two doors were designed separately and genuinely differ.

errorPOST /callsPOST /uploadsWhat to do
ingest_daily_audio_allowance_exhausted429402Retry after midnight
ingest_monthly_audio_allowance_exhausted429402Wait for the month to turn — midnight will not help
ai_budget_exhausted429402Stop. Only an operator can clear it
ingest_allowance_unknown429402One more attempt, then escalate
ingest_subscription_lapsed402402Stop. Needs a renewal, not a retry
insufficient_scope403403Fix the key's scopes. Never retry
upload_ticket_invalid422The ticket is unreadable, expired, or was not issued to you. Request a new upload
upload_not_received422No file has arrived at that upload yet. Complete the PUT and submit again — the ticket is still good
upload_too_large422The object exceeds 200 MB. Stop. It has been discarded and the same file will never be accepted
upload_not_audio422The object does not begin like a recognised audio container. Stop. It has been discarded
upload_unsupported503This deployment's storage cannot presign uploads. An operator problem; retry after a deploy
webhook_url_refusedFix the URL. Never retry (webhook routes)
webhook_url_takenThis URL is already registered for your organization. Not retryable; reuse the existing endpoint or pick a different URL
webhook_endpoint_not_foundNo endpoint with that id. Not retryable

A dash means that door never answers with that code — not that the status is unknown. The three webhook_* codes belong to the /webhooks routes, upload_unsupported to the presign door alone, and the four upload_* refusals to POST /calls, which is where a ticket is opened and the object it names is measured. See uploading audio for the full upload sequence.

The upload_* family is not the same branch as an allowance refusal, and conflating them is expensive. A 402 says the recording was fine and your account was not, so retrying after the window resets works. upload_too_large and upload_not_audio say the file itself will never be accepted — the object has already been discarded — so a client that retries them burns its upload budget forever. upload_not_received is the one exception in that family: the ticket is still good, so completing the transfer and submitting again succeeds.

The allowance figures come back in audio minutes, and the two doors shape them differently.

POST /calls returns them flat, and only the two calendar-bound codes carry a full set:

errorWhat data contains
ingest_daily_audio_allowance_exhaustedlimit_audio_minutes, used_audio_minutes, remaining_audio_minutes, resets_at
ingest_monthly_audio_allowance_exhaustedthe same four
ai_budget_exhaustedused_audio_minutes only, always 0
ingest_allowance_unknownused_audio_minutes only, always 0
ingest_subscription_lapsedused_audio_minutes only, always 0

The last three carry no figures because there are none to report — a spend ceiling, an unreadable organization and a lapsed subscription are not measured in audio minutes. A key whose value would be null is dropped from the response rather than sent as null, so test for a key's presence, not for a null value.

POST /uploads nests them instead — data.allowance.limit, .used, .remaining, .resets_at — and keeps all four keys, using null where /calls would omit.

On POST /calls, a missing key means there is nothing to report, not that the value is null. Only ingest_daily_audio_allowance_exhausted and ingest_monthly_audio_allowance_exhausted are calendar-bound; the other three clear when an operator acts, not when a clock turns, so resets_at — and every other figure but used_audio_minutes — is simply absent. Check for a key's presence, not for a null value. (POST /uploads differs here too: it keeps every key, resets_at included, and sets it to null.)

ingest_subscription_lapsed is not a suspension. Reads, exports and sign-in keep working — only new calls are refused. It clears when the subscription is renewed in the platform console.

A 429 is not always a rate limit

This is the one that breaks integrations. On POST /calls, two different things return 429, and they need opposite handling:

Throttled. The bare framework body, plus a Retry-After header:

{ "message": "Too Many Attempts." }

Back off and retry. This is what rate limits describes.

Refused on allowance. The full envelope, and no Retry-After at all:

{
  "success": false,
  "message": "…",
  "error": "ai_budget_exhausted",
  "data": { "used_audio_minutes": 0 }
}

Note how little is there: no limit_audio_minutes, no remaining_audio_minutes, no resets_at — a budget refusal has no ceiling and no window to report, so those keys are omitted rather than sent as null, and used_audio_minutes reports a bare 0 rather than a real count. A daily or monthly refusal carries the full four-key set instead. Since a budget refusal's data carries essentially nothing to branch on, the discriminator has to be the error key itself, not anything inside data.

The discriminator is the presence of an error key. A client that assumes every 429 means "throttled, back off exponentially" will retry ai_budget_exhausted forever — nothing about time clears it. Check for error before you schedule a retry.

Two different 422 shapes

POST /webhooks can fail validation two ways, and they do not share a body.

A field failing validation returns the framework's own shape — note there is no success, no error, no data:

{ "message": "The url field is required.", "errors": { "url": ["The url field is required."] } }

A value that is well-formed but refused by application logic returns the standard envelope, with a hint restating the rule that was violated:

{
  "success": false,
  "message": "…",
  "error": "webhook_url_refused",
  "data": { "hint": "Endpoints must be https and must resolve to a public address." }
}

Branch on the presence of errors versus error.

Authentication and authorisation

401 means the key is missing, malformed, unknown, or revoked. All four return the same message on purpose, so responses cannot be used to discover which keys exist.

403 means the key is valid but lacks a scope; data.required_scope names the one needed. Scopes are fixed when the key is minted, so this is never worth retrying.

On this page