Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mascot.bot/llms.txt

Use this file to discover all available pages before exploring further.

Every authorization failure carries a semantic 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:
ClassDomain
LipsyncErrorBase. Also used directly for client-side codes (e.g. bad_timeline).
LicenseErrorAuthorization at init.
RefusedErrorHard refusal (init or refresh) — not retryable.
NetworkErrorTransport failure reaching the edge service.
EngineErrorInference/runtime failure.
Branch on error.code, not the subclass. Codes are stable; the class is just the domain bucket.
import { RefusedError, NetworkError, EngineError } from "@mascotbot/react";

function StatusUI() {
  const { status, error } = useMascot();
  if (status !== "error" && status !== "refused") return null;
  if (error instanceof RefusedError) {
    switch (error.code) {
      case "key_disabled":
      case "subscription_canceled":          return <ReSubscribe />;
      case "subscription_past_due_expired":  return <UpdateCard />;
      case "key_expired":                    return <RotateKey />;
      case "invalid_api_key":                return <CheckKey />;
      case "prod_key_on_localhost":          return <UseDevKey />;
      case "dev_key_on_public_domain":       return <UseProdKey />;
      case "origin_not_allowed":             return <CheckAllowlist />;
      case "session_expired":                return <ReloadPrompt />;
    }
  }
  if (error instanceof NetworkError) return <p>Network issue — retry</p>;
  if (error instanceof EngineError)  return <p>Inference error — refresh</p>;
  return <p>{error?.message}</p>;
}

Authorization codes

The edge worker verifies the key and maps the rejection to one envelope.
HTTPcodeFires whenRecommended UI
401missing_bearerNo Authorization header.Fix the integration (key not sent).
401invalid_api_keyKey not recognized — typo’d or never existed.”Check the key at app.mascot.bot/api-keys.”
401key_expiredDev keys auto-expire 30 days after creation.”Mint a fresh key.”
402key_disabledKey disabled — canonical canceled/revoked path.”Re-subscribe, or mint a new key.”
402subscription_past_due_expiredPayment failed and the grace period ran out.”Update your card.”
402subscription_canceledSubscription canceled (identity preserved).”Re-subscribe.”
403wrong_key_scopeKey used on a surface it is not authorized for.”Issue the matching key type.”
403prod_key_on_localhostProduction key from localhost.”Use a development key locally.”
403dev_key_on_public_domainDevelopment key from a public origin.”Use a production publishable key.”
403origin_not_allowedProduction key + origin not on the allow-list.”Add the origin at app.mascot.bot/security.”
403origin_allowlist_emptyProduction key with no origins configured.”Configure the allow-list.”
403missing_originProduction key with no Origin header.Fix the request.
429rate_limitedKey verification throttled. Transient.”Try again in a few seconds.”
404session_expiredThe 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)

Some LipsyncErrors are thrown entirely client-side and still carry .code:
ClasscodeFires whenTreat as
LipsyncErrorbad_timelineparseTimeline(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.
import { parseTimeline, LipsyncError } from "@mascotbot/core";

try {
  playback.setTimeline(parseTimeline(JSON.parse(stored)));
} catch (err) {
  if (err instanceof LipsyncError && err.code === "bad_timeline") {
    // re-run processAudio() and re-persist
  }
}
See Offline lip sync and the timeline model.

Wire format

Every error response is JSON with Content-Type: application/json; the HTTP status is the envelope status:
{ "code": "key_disabled", "message": "This API key has been disabled. …" }
The SDK parses this into the typed error’s .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.