code and an actionable
message. The SDK surfaces them as typed errors so your app can show the
right next step (“re-subscribe”, “update card”, “use a dev key”) instead of a
flat 401.
The taxonomy
Five classes, mapped to failure domains:| Class | Domain |
|---|---|
LipsyncError | Base. Also used directly for client-side codes (e.g. bad_timeline). |
LicenseError | Authorization at init. |
RefusedError | Hard refusal (init or refresh) — not retryable. |
NetworkError | Transport failure reaching the edge service. |
EngineError | Inference/runtime failure. |
Authorization codes
The edge worker verifies the key and maps the rejection to one envelope.| HTTP | code | Fires when | Recommended UI |
|---|---|---|---|
| 401 | missing_bearer | No Authorization header. | Fix the integration (key not sent). |
| 401 | invalid_api_key | Key not recognized — typo’d or never existed. | ”Check the key at app.mascot.bot/api-keys.” |
| 401 | key_expired | Dev keys auto-expire 30 days after creation. | ”Mint a fresh key.” |
| 402 | key_disabled | Key disabled — canonical canceled/revoked path. | ”Re-subscribe, or mint a new key.” |
| 402 | subscription_past_due_expired | Payment failed and the grace period ran out. | ”Update your card.” |
| 402 | subscription_canceled | Subscription canceled (identity preserved). | ”Re-subscribe.” |
| 403 | wrong_key_scope | Key used on a surface it is not authorized for. | ”Issue the matching key type.” |
| 403 | prod_key_on_localhost | Production key from localhost. | ”Use a development key locally.” |
| 403 | dev_key_on_public_domain | Development key from a public origin. | ”Use a production publishable key.” |
| 403 | origin_not_allowed | Production key + origin not on the allow-list. | ”Add the origin at app.mascot.bot/security.” |
| 403 | origin_allowlist_empty | Production key with no origins configured. | ”Configure the allow-list.” |
| 403 | missing_origin | Production key with no Origin header. | Fix the request. |
| 429 | rate_limited | Key verification throttled. Transient. | ”Try again in a few seconds.” |
| 404 | session_expired | The referenced session is gone (refresh only). Hard refusal. | ”Reload the page to start a fresh session.” |
session_expired realistically happens when a tab is backgrounded long enough
that throttled refresh ticks let the session lapse. It is never recoverable by
retry — only a fresh init (a reload) recovers, so prompt the user immediately.
Client-side codes (no network)
SomeLipsyncErrors are thrown entirely client-side and still carry
.code:
| Class | code | Fires when | Treat as |
|---|---|---|---|
LipsyncError | bad_timeline | parseTimeline(input) rejected a persisted/untrusted timeline — version mismatch, non-positive frameMs, non-monotonic cue offsets, missing leading t: 0, out-of-range viseme id, or wrong field types. | ”Regenerate via client.processAudio()” — not a license/network condition. |
Wire format
Every error response is JSON withContent-Type: application/json; the HTTP
status is the envelope status:
.code / .message. When a body
is not JSON (5xx, network blip) the SDK synthesizes a status-derived envelope,
so .code is always present.
Next
Licensing & keys
Why these refusals happen.
API conventions
Why the taxonomy is shaped this way.
Troubleshooting
Fixing them in practice.