Lecturia Developers

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.

CodeHTTPCauseFix
already_delivered409Force-retry of a webhook delivery that already succeededNothing to retry — emit a fresh event if you need redelivery
already_in_flight409Force-retry of a webhook delivery the worker is posting right nowWait for the attempt to finish, then retry if it failed
already_scheduled409Reserved for a future distinct "retrying" delivery statusNot emitted today — treat like already_in_flight
api_access_requires_credits402Free account that never purchased credits tried to use a paid API capabilityBuy credits at app.lecturia.ai (purchases are web-only)
api_key_already_revoked409Repeat DELETE /keys/{id} on an already-revoked keyNothing to do — the key is already dead
api_key_expired401The key passed its expires_atMint a new key
api_key_not_allowed_on_bff403An API key was sent to a /v1/bff/* (web-app) routeUse the SaaS surface (/v1/saas/*) with API keys
api_key_revoked401The key was revokedMint a new key
bearer_not_allowed_on_saas403A session JWT was sent where an API key is requiredSend Authorization: Bearer lk_... — see Authentication
chunk_not_found404The chunk index does not exist on the materialCheck GET /materials/{id}/chunks for valid indexes
chunk_not_ready409The chunk exists but has no streamable audio / readable text yetWait for chunk.completed (webhook) or poll the chunk status
email_in_use409Registration with an email that already has an accountLog in instead, or use another address
email_not_confirmed403Login before the confirmation link was clickedConfirm the address from your inbox, then log in
ereader_locked409Convert named a different e-reader than the material's first conversionUse details.locked_ereader_id, or upload the material again for a new voice
ereader_not_found404Unknown e-reader idPick an id from GET /ereaders
ereader_tier_not_available403The e-reader exists but is not on your account's tierChoose another voice or upgrade the account
events_required400Webhook registered with an empty events arraySubscribe to at least one event type
file_too_large413An uploaded file exceeded the per-file cap (details.max_bytes)Split or shrink the file
idempotency_key_reuse409Same Idempotency-Key reused for a materially different requestUse a fresh key per distinct request
insufficient_balance402Too few credits for the requested operationTop up at app.lecturia.ai; check GET /credits/balance first
insufficient_scope403The key lacks the scope the endpoint requiresUse (or mint) a key with the needed scope — see Authentication
internal_error500Unexpected server-side faultRetry with backoff; report the request_id if it persists
invalid_api_key401The presented key is not a known active keyRe-copy the key; check for truncation or whitespace
invalid_credentials401Login email/password rejected (same message whether the account exists)Check the credentials; use password reset in the app
invalid_event_type400Webhook events contains types Lecturia does not emitUse only types from details.allowed
invalid_idempotency_key400Idempotency-Key header over 128 chars or outside [A-Za-z0-9_-]Shorten / restrict the key alphabet
invite_invalid403The registration invite token can't be honoured (expired, consumed, unknown — deliberately unspecified)Request a fresh invitation
invite_required403Registration without a token while the beta is invitation-onlyJoin the waitlist and register with the invite you receive
material_chunks_already_converted409The material's chunks are already convertedNothing to do — fetch the audio
material_not_found404Unknown material id (malformed ids are also 404 — no enumeration)Check GET /materials
missing_api_key401No Authorization header reached the APISend Authorization: Bearer lk_...
not_found404Generic resource-not-foundCheck the id and the endpoint path
rate_limited429A velocity limit was exceededBack off and retry; honour Retry-After when present
request_too_large413The request body exceeded the endpoint's buffer capSend a smaller body
scope_escalation403Key minting requested scopes beyond the calling key's (details.requested vs details.held)Request a subset of your key's scopes
service_unavailable503A protective control (e.g. the registration velocity limiter) cannot answer, so the request was refused rather than served unprotectedYou did nothing wrong — retry after Retry-After
text_empty422Empty text suppliedSend non-empty text
text_too_long422text 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_endpoints422The account already owns 20 active webhook endpointsDisable an endpoint to free a slot
tts_provider_error502An upstream TTS provider failedRetry with backoff; check status.lecturia.ai
url_host_not_allowed400Webhook URL host is internal / link-local / metadata (SSRF denylist)Use a publicly routable HTTPS host
url_must_be_https400Webhook URL is not https://Webhook destinations must be HTTPS
validation_failed422One or more request fields failed validation (details.fields names them)Fix the named fields
weak_password400Registration 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.