Errors and rate limits

Every error response uses one envelope, whatever the endpoint:

json
{
  "success": false,
  "error": {
    "type": "rate_limited",
    "message": "Too many requests, slow down"
  }
}

Branch on error.type, not on the message text; messages can change, types will not.

Error types

  • invalid_input (400, and 405 for a wrong HTTP method): the request body, query or method is wrong. Fix the request against the API reference; the message says which field failed. Also returned by POST /posts/schedule when a platform's post metadata is missing on the video.
  • unauthorized (401): missing, malformed or revoked API key. Check the Authorization: Bearer fl_live_... (or X-API-Key) header; with the CLI, re-run faceless login or fix FACELESS_API_KEY.
  • insufficient_credits (402): the team cannot pay for a credit-costing operation (POST /videos, series episodes). Nothing was charged or created. Top up at https://faceless.so/billing; do not retry until the balance changes.
  • forbidden_scope (403): the key is valid but lacks a required scope. Create a key with the needed scopes in team settings at https://faceless.so/team; GET /me shows the scopes a key has.
  • usage_limit_reached (403): the team's plan caps its number of video projects and the cap is hit. Upgrading the plan (or deleting old projects) fixes it; changing keys does not.
  • not_found (404): the resource id does not exist or belongs to another team. Also returned for malformed object ids.
  • conflict (409): the request clashes with current state, most commonly cancelling a post that is no longer scheduled, or reusing an Idempotency-Key with a different request body. Do not blind-retry; re-read the resource state first.
  • rate_limited (429): too many requests for this key. Wait for the number of seconds in the Retry-After header, then retry. Default limit 300 per 60s, with tighter per-operation limits on creation and publishing endpoints (see credits and limits).
  • internal_error (500 and other 5xx): something failed on our side. Retry with exponential backoff; if it persists, contact support with the request path and time.

HTTP status to type mapping

  • 400: invalid_input
  • 401: unauthorized
  • 402: insufficient_credits
  • 403: forbidden_scope or usage_limit_reached
  • 404: not_found
  • 405: invalid_input (method not allowed on this endpoint)
  • 409: conflict
  • 429: rate_limited
  • 5xx: internal_error

Idempotency and the 409 conflict

Mutating operations marked idempotent in the API reference (POST /videos, POST /videos/captions, POST /videos/{id}/render, POST /series, POST /series/{id}/generate, POST /posts, POST /posts/schedule, POST /assets) accept an Idempotency-Key header (any unique string, for example a UUID). Successful responses are stored for 24 hours; retrying with the same key and the same body replays the stored response instead of re-running the operation, which makes timeouts safe to retry and prevents double credit charges. The CLI generates a key automatically for every mutating command; MCP tools accept an idempotencyKey argument with the same semantics.

Reusing a key with a different body is treated as a bug in your client and returns 409:

json
{
  "success": false,
  "error": {
    "type": "conflict",
    "message": "This Idempotency-Key was already used with a different request body"
  }
}

Generate a fresh key per logical operation, and reuse it only for retries of exactly that operation.

Async failures are not HTTP errors

A video whose generation fails reports it through polling, not through an error status: GET /videos/{id}/status returns 200 with status: "failed" and the reasons in errorMessages. Likewise a render ends as status: "error" on GET /renders/{id}. Poll to terminal states and branch on the data, not just on HTTP codes.