Error codes
Every 4xx/5xx response from https://api.lecturia.ai/v1/saas uses one
envelope:
{
"error": {
"code": "insufficient_balance",
"message": "human-readable explanation",
"details": { "field": "..." },
"request_id": "01J9Z8REQG"
}
}
Match on code — the strings below are stable and never renamed (new codes
may appear; a rename would be a breaking change). message is for humans
and may change. details carries per-code extras where noted.
request_id echoes the X-Request-Id response header — include it in
support requests.
Link to any code directly: /docs/errors#the_code.
| Code | HTTP | Cause | Fix |
|---|---|---|---|
already_delivered | 409 | Force-retry of a webhook delivery that already succeeded | Nothing to retry — emit a fresh event if you need redelivery |
already_in_flight | 409 | Force-retry of a webhook delivery the worker is posting right now | Wait for the attempt to finish, then retry if it failed |
already_scheduled | 409 | Reserved for a future distinct "retrying" delivery status | Not emitted today — treat like already_in_flight |
api_access_requires_credits | 402 | Free account that never purchased credits tried to use a paid API capability | Buy credits at app.lecturia.ai (purchases are web-only) |
api_key_already_revoked | 409 | Repeat DELETE /keys/{id} on an already-revoked key | Nothing to do — the key is already dead |
api_key_expired | 401 | The key passed its expires_at | Mint a new key |
api_key_not_allowed_on_bff | 403 | An API key was sent to a /v1/bff/* (web-app) route | Use the SaaS surface (/v1/saas/*) with API keys |
api_key_revoked | 401 | The key was revoked | Mint a new key |
bearer_not_allowed_on_saas | 403 | A session JWT was sent where an API key is required | Send Authorization: Bearer lk_... — see Authentication |
chunk_not_found | 404 | The chunk index does not exist on the material | Check GET /materials/{id}/chunks for valid indexes |
chunk_not_ready | 409 | The chunk exists but has no streamable audio / readable text yet | Wait for chunk.completed (webhook) or poll the chunk status |
email_in_use | 409 | Registration with an email that already has an account | Log in instead, or use another address |
email_not_confirmed | 403 | Login before the confirmation link was clicked | Confirm the address from your inbox, then log in |
ereader_locked | 409 | Convert named a different e-reader than the material's first conversion | Use details.locked_ereader_id, or upload the material again for a new voice |
ereader_not_found | 404 | Unknown e-reader id | Pick an id from GET /ereaders |
ereader_tier_not_available | 403 | The e-reader exists but is not on your account's tier | Choose another voice or upgrade the account |
events_required | 400 | Webhook registered with an empty events array | Subscribe to at least one event type |
file_too_large | 413 | An uploaded file exceeded the per-file cap (details.max_bytes) | Split or shrink the file |
idempotency_key_reuse | 409 | Same Idempotency-Key reused for a materially different request | Use a fresh key per distinct request |
insufficient_balance | 402 | Too few credits for the requested operation | Top up at app.lecturia.ai; check GET /credits/balance first |
insufficient_scope | 403 | The key lacks the scope the endpoint requires | Use (or mint) a key with the needed scope — see Authentication |
internal_error | 500 | Unexpected server-side fault | Retry with backoff; report the request_id if it persists |
invalid_api_key | 401 | The presented key is not a known active key | Re-copy the key; check for truncation or whitespace |
invalid_credentials | 401 | Login email/password rejected (same message whether the account exists) | Check the credentials; use password reset in the app |
invalid_event_type | 400 | Webhook events contains types Lecturia does not emit | Use only types from details.allowed |
invalid_idempotency_key | 400 | Idempotency-Key header over 128 chars or outside [A-Za-z0-9_-] | Shorten / restrict the key alphabet |
invite_invalid | 403 | The registration invite token can't be honoured (expired, consumed, unknown — deliberately unspecified) | Request a fresh invitation |
invite_required | 403 | Registration without a token while the beta is invitation-only | Join the waitlist and register with the invite you receive |
material_chunks_already_converted | 409 | The material's chunks are already converted | Nothing to do — fetch the audio |
material_not_found | 404 | Unknown material id (malformed ids are also 404 — no enumeration) | Check GET /materials |
missing_api_key | 401 | No Authorization header reached the API | Send Authorization: Bearer lk_... |
not_found | 404 | Generic resource-not-found | Check the id and the endpoint path |
rate_limited | 429 | A velocity limit was exceeded | Back off and retry; honour Retry-After when present |
request_too_large | 413 | The request body exceeded the endpoint's buffer cap | Send a smaller body |
scope_escalation | 403 | Key minting requested scopes beyond the calling key's (details.requested vs details.held) | Request a subset of your key's scopes |
| 503 | A protective control (e.g. the registration velocity limiter) cannot answer, so the request was refused rather than served unprotected | You did nothing wrong — retry after Retry-After | |
text_empty | 422 | Empty text supplied | Send non-empty text |
text_too_long | 422 | text exceeds the endpoint's maximum (5,000 chars on tts:synthesize) | Split the text, or use the materials pipeline for long content |
too_many_webhook_endpoints | 422 | The account already owns 20 active webhook endpoints | Disable an endpoint to free a slot |
tts_provider_error | 502 | An upstream TTS provider failed | Retry with backoff; check status.lecturia.ai |
url_host_not_allowed | 400 | Webhook URL host is internal / link-local / metadata (SSRF denylist) | Use a publicly routable HTTPS host |
url_must_be_https | 400 | Webhook URL is not https:// | Webhook destinations must be HTTPS |
validation_failed | 422 | One or more request fields failed validation (details.fields names them) | Fix the named fields |
weak_password | 400 | Registration password fails the policy (details.requirements) | Meet the listed requirements |
Retryable classes: rate_limited and service_unavailable (back off,
honour Retry-After), internal_error and tts_provider_error
(exponential backoff). Everything else is deterministic — fix the request
instead of retrying.