# 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](/developers/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](/developers/docs/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.
