Skip to main content
Every non-2xx response uses the same envelope. Branch retry policy on type, branch UX on code, surface message to humans, and quote request_id in support tickets.
{
  "error": {
    "type": "invalid_request",
    "code": "balance_too_low",
    "message": "Team available balance is 1.5 credits, generation requires 2.0.",
    "param": null,
    "doc_url": "https://docs.aurous-labs.com/errors#balance_too_low",
    "request_id": "req_01HXMQ7Z3K8Y2ABCDEFGHJKM"
  }
}

Type taxonomy

type is one of five values. The Aurous-Request-Id response header always carries the same value as error.request_id so logging middleware doesn’t need to parse the body.
TypeHTTP statusesExample codes
invalid_request400, 402, 409, 422missing_field, invalid_format, value_out_of_range, unsupported_lora_for_mode, idempotency_key_in_use, generation_not_cancellable, balance_too_low, prompt_blocked, reference_blocked, output_moderation_rejected, output_not_available
authentication401missing_api_key, invalid_api_key, revoked_api_key
not_found404, 410resource_not_found, forbidden_resource (404 by intent — no existence leak), output_expired (410)
rate_limit429too_many_requests, concurrency_limit_exceeded
server_error500, 502, 503, 504internal_error, provider_unavailable, provider_timeout
TypeRetry?How
invalid_requestNo. Fix the input.param indicates the offending field. Don’t retry — the same body will fail again.
authenticationNo. Fix the key.Re-fetch the key from your secret store; if revoked, mint a new one.
not_foundNo.Resource doesn’t exist (or isn’t yours). Don’t retry.
rate_limitYes with backoff.Sleep Retry-After seconds (or X-RateLimit-Reset − now). See Rate limits.
server_errorYes with jitter.Exponential backoff: 1s → 2s → 4s → 8s, max ~30s. With an Idempotency-Key, retries are safe — see Idempotency.

One example per type

invalid_request — 400

curl -X POST https://api.aurous-labs.com/v1/images \
  -H "X-Api-Key: $AUROUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"size": "2k_1_1"}'
HTTP/1.1 400 Bad Request
Aurous-Request-Id: req_01HXMQ7Z3K8Y2ABCDEFGHJKM
Content-Type: application/json

{
  "error": {
    "type": "invalid_request",
    "code": "missing_field",
    "message": "prompt: must be a string, prompt: must not be empty",
    "param": "prompt",
    "doc_url": "https://docs.aurous-labs.com/errors#missing_field",
    "request_id": "req_01HXMQ7Z3K8Y2ABCDEFGHJKM"
  }
}

authentication — 401

curl https://api.aurous-labs.com/v1/balance \
  -H "X-Api-Key: al_live_invalid"
HTTP/1.1 401 Unauthorized
Aurous-Request-Id: req_01HXMQ7Z3K8Y2ABCDEFGHJKM
Content-Type: application/json

{
  "error": {
    "type": "authentication",
    "code": "invalid_api_key",
    "message": "API key is invalid or has been revoked.",
    "param": null,
    "doc_url": "https://docs.aurous-labs.com/errors#invalid_api_key",
    "request_id": "req_01HXMQ7Z3K8Y2ABCDEFGHJKM"
  }
}

not_found — 404

A 404 covers both “doesn’t exist” and “exists but not yours.” We never reveal which — same Stripe stance, no existence-leak oracle.
curl https://api.aurous-labs.com/v1/images/img_99999999999999999999999999 \
  -H "X-Api-Key: $AUROUS_API_KEY"
HTTP/1.1 404 Not Found
Aurous-Request-Id: req_01HXMQ7Z3K8Y2ABCDEFGHJKM
Content-Type: application/json

{
  "error": {
    "type": "not_found",
    "code": "resource_not_found",
    "message": "Generation not found.",
    "param": null,
    "doc_url": "https://docs.aurous-labs.com/errors#resource_not_found",
    "request_id": "req_01HXMQ7Z3K8Y2ABCDEFGHJKM"
  }
}

rate_limit — 429

HTTP/1.1 429 Too Many Requests
Aurous-Request-Id: req_01HXMQ7Z3K8Y2ABCDEFGHJKM
Retry-After: 12
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714752912
Content-Type: application/json

{
  "error": {
    "type": "rate_limit",
    "code": "too_many_requests",
    "message": "Rate limit exceeded for images_post. Retry after 12s.",
    "param": null,
    "doc_url": "https://docs.aurous-labs.com/errors#too_many_requests",
    "request_id": "req_01HXMQ7Z3K8Y2ABCDEFGHJKM"
  }
}
Sleep Retry-After seconds and retry. Don’t hammer — the bucket only refills at the sustained rate.

server_error — 5xx

Treat as transient. Retry with exponential backoff. If you sent an Idempotency-Key, the retry is safe even if the original request actually committed.
HTTP/1.1 503 Service Unavailable
Aurous-Request-Id: req_01HXMQ7Z3K8Y2ABCDEFGHJKM
Content-Type: application/json

{
  "error": {
    "type": "server_error",
    "code": "provider_unavailable",
    "message": "Image provider is unavailable. Please retry.",
    "param": null,
    "doc_url": "https://docs.aurous-labs.com/errors#provider_unavailable",
    "request_id": "req_01HXMQ7Z3K8Y2ABCDEFGHJKM"
  }
}

All error codes

Every error envelope’s doc_url is https://docs.aurous-labs.com/errors#<code>. Each section below has an anchor matching the code so the link lands on the exact paragraph.

invalid_request codes — 400 / 402 / 409

missing_field

A required field was absent from the request body or query string. param names the field. HTTP 400. Don’t retry — supply the missing field.

invalid_format

A field is present but malformed (wrong shape, wrong enum value, wrong opaque-ID prefix). param names the field. HTTP 400.

value_out_of_range

A numeric or array-length field is outside its accepted range (e.g. count > 4, reference_image_urls.length > 6, guidance_scale < 1). HTTP 400.

parameter_invalid_combination

You sent two fields together that are mutually exclusive (e.g. size AND custom width/height on POST /v1/images). HTTP 400. param names the offending input. Pick one of the two paths per request.

mutually_exclusive_input

You sent two body inputs that the endpoint accepts independently but rejects together (e.g. character_id AND reference_image_urls on POST /v1/images). HTTP 400. Distinct from parameter_invalid_combination — this code is reserved for body-input pairs whose semantic disambiguation requires you to pick one. param names the offending input. (See missing_field above. Same code is also returned when a half-supplied input is detected — e.g. width without height on POST /v1/images.)

character_not_ready

You referenced a character_id that exists but is not in status: ready (it’s synthesizing, reviewing, failed, or soft-deleted). HTTP 400. Wait for the character’s status to flip via GET /v1/characters/{id} or webhook, or pick a different character.

unsupported_lora_for_mode

The LoRA you referenced isn’t compatible with the inference mode the request would dispatch (image LoRA on /v1/videos, or vice versa). HTTP 400. Pick a LoRA from the matching catalog.

generation_not_cancellable

You called POST /v1/images/{id}/cancel on a generation that’s already terminal (succeeded, failed, cancelled, expired, or moderation_rejected). HTTP 400. Idempotent — calling cancel on a row whose hold already resolved is a no-op.

prompt_blocked

Pre-dispatch moderation classifier rejected the prompt. HTTP 400. No row is inserted, so there’s nothing to retrieve via GET /v1/images/{id}. (A future date-pin will insert a moderation_rejected row and fire image.moderation_rejected instead — see the Changelog.)

reference_blocked

Pre-dispatch moderation classifier rejected one of the reference images. HTTP 400. Same disposition as prompt_blocked.

output_moderation_rejected

Post-generation classifier rejected the output. HTTP 400. The hold is released; no charge. The reason ID is logged on the inference row.

unknown_version

The Aurous-Version header value is not in the published catalog (see the Changelog). HTTP 400. Use a date-pin advertised on the changelog or omit the header to fall back to your team default.

balance_too_low

The team’s available balance (credits − pending holds) is less than the cost of the requested generation. HTTP 402. Top up via the dashboard or wait for pending holds to commit/release.

idempotency_key_in_use

Returned in three cases, all HTTP 409: (1) you sent the same Idempotency-Key with a different request body; (2) you reused the same key across different routes (e.g. /v1/images and /v1/videos); or (3) a request with this key is still in flight — the first call hasn’t finished yet. For (1) and (2), use a fresh key (or resend the original body to replay). For (3), wait briefly and retry the same key; once the original completes you’ll receive its replayed response. See Idempotency.

authentication codes — 401

missing_api_key

No X-Api-Key header was sent. HTTP 401. Add the header — see Authentication.

invalid_api_key

The X-Api-Key header value is malformed, unknown, or no longer authorized for the requested route. HTTP 401. Re-fetch the key from your secret store; if it was rotated, mint a new one in the dashboard.

revoked_api_key

The key existed but has been revoked. HTTP 401. Mint a new key in /dashboard/api-keys.

not_found codes — 404

resource_not_found

The resource doesn’t exist (or doesn’t belong to the requesting team — see forbidden_resource). HTTP 404. Don’t retry.

forbidden_resource

The resource exists but belongs to another team. HTTP 404 (we return 404 instead of 403 — same Stripe stance, no existence-leak oracle).

output_expired

The generation reached status: succeeded and produced output, but the stored output URL has aged past its retention window. HTTP 410.
  • Image outputs are retained ~7 days after generation.
  • Video outputs are retained ~24 hours after generation.
After that, GET /v1/images/{id}/output/{n} and GET /v1/videos/{id}/output return 410 Gone with this code. Save copies of outputs you want to keep — long-term storage is intentionally not part of the platform. To get fresh outputs, create a new generation with the same prompt.

output_not_available (422) — terminal-status-without-output

output_not_available

The generation reached a terminal status that never produced output (failed, cancelled, moderation_rejected, or polling-timeout expired) — or is still in-flight (pending / processing). HTTP 422. Distinct from output_expired: output_expired means the URLs once existed and aged out, output_not_available means they never existed. Check GET /v1/images/{id} (or GET /v1/videos/{id}) for the row’s status and (when failed) error_message, then create a new generation.

rate_limit codes — 429

too_many_requests

You exceeded the rate limit for this endpoint class. HTTP 429. Sleep Retry-After seconds and try again. See Rate limits.

concurrency_limit_exceeded

You exceeded the per-team concurrent-in-flight cap (default 10 generations in pending/processing). HTTP 429. Wait for in-flight work to settle and retry; or apply backpressure in your client.

server_error codes — 5xx

internal_error

The platform hit an unexpected condition. HTTP 500. Retry with exponential backoff. With an Idempotency-Key, retries are safe.

provider_unavailable

The upstream image / video provider is unhealthy. HTTP 503. Retry with exponential backoff.

provider_timeout

The upstream image / video provider didn’t return within the platform’s polling window. HTTP 504. Retry with exponential backoff.

chat_provider_unavailable

The upstream chat model is temporarily unavailable. HTTP 502. Any held credits are released. Retry with exponential backoff — with an Idempotency-Key on a non-streamed request, retries are safe. See also: chat_provider_unavailable.

chat_provider_request_invalid

The platform sent a malformed request to the upstream model. HTTP 500. Treated as a platform-side bug; engineering is paged. Held credits are released. Retry with backoff. See also: chat_provider_request_invalid.

chat_provider_auth_failed

The platform’s credential with the upstream model failed. HTTP 500. Not a problem with your X-Api-Key. On-call paged. Retry after a short delay. See also: chat_provider_auth_failed.

chat_provider_unknown_error

The upstream returned an error the platform’s mapping table doesn’t yet recognize. HTTP 502. Engineering will add the mapping; treat as transient. See also: chat_provider_unknown_error.

LLM chat + embeddings — invalid_request codes

model_not_found

The model slug is unknown for your team. HTTP 404. List available models with GET /v1/models. See also: model_not_found.

model_disabled

The model exists but has been deactivated. HTTP 403. Pick a different model from the listing. See also: model_disabled.

model_wrong_kind

You sent an embedding model to the chat endpoint, or vice versa. HTTP 400. Check aurous_metadata.kind on each model row. See also: model_wrong_kind.

max_tokens_exceeds_hard_cap

max_tokens exceeds the model’s max_output_tokens_hard_cap. HTTP 400. Lower the request or pick a larger-cap model. See also: max_tokens_exceeds_hard_cap.

missing_max_tokens_no_model_default

max_tokens was omitted on a model with no platform default. HTTP 400. Pass max_tokens explicitly. See also: missing_max_tokens_no_model_default.

max_input_tokens_exceeded

Prompt is over the model’s context window. HTTP 400. Trim input or pick a larger model. See also: max_input_tokens_exceeded.

tool_choice_required_unsupported

tool_choice: "required" requested on a model whose capabilities don’t include it. HTTP 400. Use tool_choice: "auto" or pick a capable model. See also: tool_choice_required_unsupported.

response_format_too_large

JSON schema in response_format exceeds the platform’s payload cap. HTTP 400. Trim the schema. See also: response_format_too_large.

response_format_too_deep

JSON schema in response_format nests deeper than the parser’s cap. HTTP 400. Flatten via $defs references. See also: response_format_too_deep.

chat_cancel_target_not_found

The cancel id doesn’t exist for your team. HTTP 404. No existence leak across teams. See also: chat_cancel_target_not_found.

chat_cancel_target_already_terminal

The chat completion is already in a terminal state. HTTP 409. Idempotency hint, not a bug. Read the final-state record. See also: chat_cancel_target_already_terminal.

chat_cancel_target_not_cancellable

The record is non-terminal but the cancel can’t take effect (sync call already returned, or in-flight on a different deploy instance). HTTP 409. See also: chat_cancel_target_not_cancellable.

LLM embeddings — invalid_request codes

embeddings_batch_not_supported

input was sent as an array of pure strings. HTTP 400. v1.0 multimodal embeddings would concatenate batched text into one combined vector (opposite of OpenAI’s N→N semantics), so the platform rejects the shape explicitly. Loop client-side for N→N, or pass a content-parts array for one combined embedding. See also: embeddings_batch_not_supported.

embeddings_input_too_many_items

input content-parts array exceeds the per-request caps (16 total parts, 8 image_url parts). HTTP 400. Split into multiple requests. See also: embeddings_input_too_many_items.

embeddings_video_unsupported

video_url parts are not accepted on v1 embeddings as of 2026-05-24. The provider folds video frames into the visual billing bucket, so the previously published video rate never actually fired — to keep the receipt honest we removed the shape. Submit text or image_url parts only; image inputs bill at the visual rate. HTTP 400. See also: embeddings_video_unsupported. Renamed 2026-05-24 from embeddings_video_too_many_parts (which previously fired only on 2-or-more videos). Integrations that caught the old code on a single-video payload should switch to embeddings_video_unsupported and remove the video_url part entirely.

LLM embeddings — server_error codes

embeddings_provider_unknown_error

The upstream embedding model returned an error the platform’s mapping table doesn’t yet recognize. HTTP 502. Engineering will add the mapping; treat as transient and retry with backoff. See also: embeddings_provider_unknown_error.

LLM chat + embeddings — rate_limit codes

tpm_rate_limit_exceeded

Tokens-per-minute bucket exhausted for your team. HTTP 429. Sleep Retry-After seconds; see Rate limits. See also: tpm_rate_limit_exceeded.

provider_rate_limited

The upstream model is throttling. HTTP 503. Retry with backoff; Retry-After is forwarded when available. See also: provider_rate_limited.

The doc_url convention

Every error code has a deterministic deep-link: https://docs.aurous-labs.com/errors#<code>. Open it in a browser to land on this page’s anchor for the code.

Using request_id for support

Every error envelope and every successful response carries a request_id (also surfaced as the Aurous-Request-Id response header). When opening a support ticket, paste at least one request_id so we can pull the exact request from server logs. Example:
Hi support — getting provider_timeout on POST /v1/images for the last hour. request_id: req_01HXMQ7Z3K8Y2ABCDEFGHJKM. Team acme. Thanks.
This shortcuts triage from “let’s look around” to “here’s the exact log line.”

Idempotency replay

When you retry with the same Idempotency-Key:
  • Success (2xx) is replayed byte-for-byte with Aurous-Idempotent-Replayed: true, and is never re-billed — even if the original request had already committed.
  • invalid_request (4xx) is not replayed. balance_too_low, prompt_blocked, invalid_format, and the rest of the invalid_request family are client-fixable: a same-key retry re-evaluates the corrected request rather than replaying the old rejection. Fix the input (top up, edit the prompt) and retry with the same key.
  • Transient server errors (5xx) are safe to retry with the same key and backoff — the key ensures you won’t create a duplicate generation or double-charge if the original had actually committed.
(idempotency_key_in_use is the one invalid_request code you may see on a retry — it’s raised by the idempotency layer itself, not replayed from cache.) See Idempotency.

Headers on every response

  • Aurous-Request-Id: req_<ulid> — quote this in support tickets.
  • Aurous-Version: YYYY-MM-DD — the API version pin applied to this response.
  • X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset — see Rate limits.
  • Retry-After (only on 429) — seconds to wait before retrying.
  • Aurous-Idempotent-Replayed: true (only on idempotency-key replays) — see Idempotency.