Skip to main content

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 (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; 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).
  • 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.