# 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",
		"hint": "Wait for the seconds in the Retry-After header, then retry. Limits are documented at https://viraloop.io/developers/docs/credits-and-limits.md."
	}
}
```

Branch on `error.type`, not on the message text; messages can change, types will not. `hint` is one fixed sentence per type describing the recovery step, meant for agents that have to act without a human reading these docs. It is informational: never parse it, and never branch on it.

Paths that match no endpoint return the same envelope with `type: "not_found"` and a 404, so one parser covers the whole API surface.

A `401` also carries a `WWW-Authenticate` header pointing at [the protected-resource metadata](https://viraloop.io/.well-known/oauth-protected-resource) (RFC 9728), which lists every scope the API supports.

## 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.
- `unauthorized` (401): missing, malformed or revoked API key. Check the `Authorization: Bearer vl_live_...` (or `X-API-Key`) header; with the CLI, re-run `viraloop login` or fix `VIRALOOP_API_KEY`.
- `insufficient_credits` (402): the team does not have enough credits for a credit-costing operation. The two credit-costing endpoints are `POST /influencers/{id}/videos` (20 credits) and the optional preview in `POST /influencers` (10 credits, skipped instead of failing when out of credits). Top up at https://viraloop.io/settings/billing; do not retry until the balance changes. Failed generations refund their credits automatically.
- `forbidden_scope` (403): the key is valid but lacks a required scope. Create a key with the needed scopes at https://viraloop.io/settings/developers; `GET /me` shows the scopes a key has.
- `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. The three common cases: cancelling a post that is no longer `scheduled`, publishing a suggestion that is already scheduled, and 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. Limits: 300 per 60s default, 10 per 300s on `POST /generations`, 60 per 300s on `POST /posts` (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`
- 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 (including `POST /generations`, `POST /posts`, `POST /influencers` and `POST /influencers/{id}/videos`) 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.
