Reference

Errors

Every error message is written for people: it says what went wrong and what to do next, and never contains secrets. You can show it to your users as-is. Use the status codes and error codes below to decide what your code does.

Two kinds of errors

  • The request is refused. POST /video/request returns a 4xx status. No job is created and nothing counts toward your limit. Fix the request and send it again.
  • The job fails later. The request was accepted, but making the video failed. GET /video/status returns HTTP 200 with status: "failed", a readable error and a machine-readable error_code.

HTTP errors

Refused requests return JSON with a detail message:

403 response
{
  "detail": "Claude Opus 5.5 needs the Pro plan. Upgrade at manimotion.dev/pricing, or use your own Anthropic or OpenRouter key on any plan."
}
StatusMeaningCommon causes
400Bad requestNo storage configured; invalid bucket credentials; unknown model id; the key in llm was rejected or can’t run the model
401Not authenticatedMissing, mistyped or revoked x-api-key
403Not allowedThe model isn’t in your plan, or only runs on your own key. See Bring your own key
404Not foundUnknown job id, or a job that belongs to another account
422Invalid fieldPrompt shorter than 10 or longer than 2000 characters; duration outside 15–180; unknown llm.provider. detail is a list naming each bad field.
429Too many requestsYour plan’s video limit is used up (Free: 3 per day, Hobby: 40 per month, Pro: 80 per month), or more than 20 requests in 10 minutes from one IP address
5xxOur problemTemporary. Retry with a backoff

Job failure codes

When status is failed, error_code is one of these:

error_codeWhat happenedWhat to do
llm_authYour AI provider rejected your keyReplace the key in Settings → AI providers (or in llm.api_key)
llm_quotaYour AI provider account is out of credits or over its limitAdd credits with the provider, then retry
llm_rate_limitYour AI provider is rate-limiting your keyWait a minute, then retry
llm_modelYour AI provider couldn’t run that model idCheck the id, or choose another model
llm_bad_requestYour AI provider refused the request (the message says why)Usually a provider-side policy or setting; try another model
llm_unavailableThe AI service is down or overloadedRetry in a few minutes
storage_failedUploading to your bucket failedCheck the bucket in Settings → Storage, then retry
generation_failedThe lecture couldn’t be madeRetry, rephrase the prompt more specifically, try another style or a stronger model

The llm_auth, llm_quota, llm_rate_limit, llm_model and llm_bad_request codes only happen on your own key. On our key, any AI problem shows as llm_unavailable.

GET /video/status (failed)
{
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "failed",
  "video_url": null,
  "error": "Anthropic rejected the API key. Check or replace your saved key in Settings → AI providers.",
  "error_code": "llm_auth",
  "model": "anthropic/claude-sonnet-5.5",
  "key_source": "byok",
  "cached": false,
  "style": null,
  "duration": null,
  "phase": "failed",
  "message": "Something went wrong"
}

Handling errors

A typical pattern with the SDKs: fall back to the default model when a model is locked, alert yourself when a key needs attention, and show everything else to the user.

errors.mjs
import {
  BadRequestError,
  PermissionDeniedError,
  RateLimitError,
  VideoFailedError,
} from "@manimotion/sdk";

async function makeVideo(prompt, model) {
  try {
    const video = await manimotion.videos.generate({ prompt, model });
    return video.videoUrl;
  } catch (err) {
    if (err instanceof PermissionDeniedError) {
      // Model not in the plan: fall back to the default model
      return makeVideo(prompt);
    }
    if (err instanceof VideoFailedError) {
      if (err.code === "llm_auth" || err.code === "llm_quota") {
        notifyOwner(err.message); // the AI key needs attention
      } else if (err.code === "llm_rate_limit" || err.code === "llm_unavailable") {
        retryLater(prompt);
      } else {
        showUser(err.message); // safe to show as-is
      }
      return null;
    }
    if (err instanceof BadRequestError || err instanceof RateLimitError) {
      showUser(err.message);
      return null;
    }
    throw err;
  }
}

Retrying safely

  • Status checks are read-only. Retry them freely on network errors and 5xx.
  • Creating a job isn’t idempotent. A retry after a timeout can start a second video, which counts toward your limit. Retry POST /video/request only when you got a clear error response.
  • For 429, wait before retrying. Plan limits reset at midnight UTC (daily) or on the 1st of the month (monthly).

SDK error classes

The SDKs turn these into typed errors. All of them extend ManimotionError; HTTP errors also carry the status code (status in JavaScript, status_code in Python) and the API’s message.

ClassWhen
BadRequestError400
AuthenticationError401
PermissionDeniedError403
NotFoundError404
ValidationError422
RateLimitError429
InternalServerError5xx
APIConnectionErrorCouldn’t reach the API
TimeoutErrorWaiting took too long (APITimeoutError in Python)
VideoFailedErrorThe job failed; code holds the error_code