Skip to content
lumalabs.ai

Error handling

Every HTTP status code, error response, rate limit scenario, and async failure mode in the Luma Agents API.

This page documents every error the Luma Agents API can return, both synchronous errors (returned immediately) and asynchronous failures (discovered when polling). Use it as a reference when building error handling into your integration.

StatusMeaningRetryable?
201Success — generation created
400Invalid request parametersNo — fix parameters
401Invalid or missing API keyNo — fix authentication
402Insufficient creditsNo — add credits
403Access denied (plan or account)No — check your plan
413Image exceeds 50 MBNo — resize image
422Bad image data (base64 or URL)No — fix image
429Rate limited (RPM or concurrent)Yes — use Retry-After header
502Upstream service unavailableYes — retry with backoff
503Image ingestion unavailableYes — retry or use base64

Synchronous errors (returned immediately) use HTTP status codes as the primary machine-readable identifier — branch on the status code, then read detail for specifics. See the quick reference table above.

Asynchronous failures (discovered when polling) include a failure_code field for programmatic handling:

failure_codeDescriptionRetryable?
content_moderatedPrompt or input image violated content guidelinesNo — modify prompt
generation_failedInternal model error during generationYes — retry same request
budget_exhaustedCredits ran out mid-generationNo — add credits, then retry
output_not_foundGenerated output could not be retrievedYes — retry same request
if generation.state == "failed":
if generation.failure_code == "content_moderated":
# Do not retry — modify the prompt
raise ValueError(f"Content policy violation: {generation.failure_reason}")
else:
# Transient error — safe to retry
retry_generation(generation)

See Asynchronous failures below for the full response structure and handling examples.


All error responses share the same shape:

{
"detail": "Human-readable error message describing what went wrong"
}

Every response — success or error — includes these headers:

HeaderDescription
X-Request-IdEchoes your X-Request-Id header, or a server-generated UUID if you didn’t send one
X-API-VersionAPI version for this response (currently 2026-04-01)

Synchronous errors on POST /v1/generations

Section titled “Synchronous errors on POST /v1/generations”

These errors are returned immediately when submitting a generation request.

The happy path. The generation was accepted and queued for processing.

Request:

Terminal window
curl -X POST https://agents.lumalabs.ai/v1/generations \
-H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A glass of iced coffee on a marble countertop, morning light streaming through a window",
"aspect_ratio": "16:9"
}'

Response (HTTP 201):

HTTP/1.1 201 Created
Content-Type: application/json
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
X-API-Version: 2026-04-01
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 1712592060
{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"type": "image",
"state": "queued",
"model": "uni-1",
"created_at": "2026-04-08T12:00:00Z",
"output": [],
"failure_reason": null,
"failure_code": null
}

Note the rate limit headers — use them to track your remaining quota proactively.


The request body contains invalid or conflicting parameters. The detail field tells you exactly what’s wrong.

Unknown model

Request:

{
"prompt": "A sunset over the ocean",
"model": "super-model-v9"
}

Response (HTTP 400):

{
"detail": "Unknown model: super-model-v9"
}

Fix: Use a supported model. Supported values are uni-1 and uni-1-max — both available to all accounts. See Models for details. Note: any future unreleased alias names will also surface as "Unknown model" (HTTP 400), not as a distinct “not on your plan” error — this is intentional, to prevent enumeration of unreleased aliases.

Unsupported generation type

Request:

{
"prompt": "A sunset over the ocean",
"type": "video"
}

Response (HTTP 400):

{
"detail": "Type 'video' is not supported for model 'uni-1'"
}

Fix: Set type to "image" or "image_edit" — the only types supported by uni-1.

Invalid aspect ratio

Request:

{
"prompt": "A sunset over the ocean",
"aspect_ratio": "4:3"
}

Response (HTTP 400):

{
"detail": "Invalid aspect_ratio '4:3'. Valid: 1:1, 1:2, 1:3, 16:9, 2:1, 2:3, 3:1, 3:2, 9:16"
}

Fix: Use one of the 9 supported aspect ratios. The Valid: list is sorted lexicographically by the server, not in any visual order.

Prompt too short (empty)

Request:

{
"prompt": ""
}

Response (HTTP 400):

{
"detail": "Prompt must be between 1 and 6000 characters"
}

Fix: Provide a prompt with at least 1 character.

Prompt too long (over 6,000 characters)

Request:

{
"prompt": "A very long prompt that exceeds 6000 characters..."
}

Response (HTTP 400):

{
"detail": "Prompt must be between 1 and 6000 characters"
}

Fix: Shorten the prompt to 6,000 characters or fewer.

Malformed image reference — both url and data provided

Each ImageRef must use either url or data, not both.

Request:

{
"prompt": "A sunset over the ocean",
"image_ref": [
{
"url": "https://example.com/photo.jpg",
"data": "iVBORw0KGgo...",
"media_type": "image/jpeg"
}
]
}

Response (HTTP 400):

{
"detail": "image_ref[0]: provide either 'url' or 'data', not both"
}

The detail is prefixed with the field path — source for the edit source image, or image_ref[i] for the reference at index i.

Fix: Remove either the url or the data/media_type fields.

Too many image references on an edit (more than 8)

For type: "image_edit", the source image occupies one reference slot, so you may only pass up to 8 additional references via image_ref.

Request:

{
"type": "image_edit",
"prompt": "Restyle this scene",
"source": {"url": "https://example.com/source.jpg"},
"image_ref": [
{"url": "https://example.com/1.jpg"},
{"url": "https://example.com/2.jpg"},
{"url": "https://example.com/3.jpg"},
{"url": "https://example.com/4.jpg"},
{"url": "https://example.com/5.jpg"},
{"url": "https://example.com/6.jpg"},
{"url": "https://example.com/7.jpg"},
{"url": "https://example.com/8.jpg"},
{"url": "https://example.com/9.jpg"}
]
}

Response (HTTP 400):

{
"detail": "image_edit supports up to 8 reference images (source occupies one slot)"
}

Fix: Reduce image_ref to 8 or fewer entries.

Source provided for type “image”

The source field is only valid for image_edit requests. If you provide it with type: "image" (or the default), the request is rejected.

Request:

{
"prompt": "A sunset over the ocean",
"type": "image",
"source": {
"url": "https://example.com/photo.jpg"
}
}

Response (HTTP 400):

{
"detail": "source is only valid for type 'image_edit'"
}

Fix: Remove the source field, or change type to "image_edit".

Source missing for type “image_edit”

The source field is required for image_edit requests.

Request:

{
"prompt": "Make the sky purple",
"type": "image_edit"
}

Response (HTTP 400):

{
"detail": "source is required for type 'image_edit'"
}

Fix: Add a source object with either a url or data field.

Base64 data without media_type

When providing inline image data, media_type is required.

Request:

{
"prompt": "A sunset over the ocean",
"image_ref": [
{
"data": "iVBORw0KGgo..."
}
]
}

Response (HTTP 400):

{
"detail": "image_ref[0]: 'media_type' is required with 'data'"
}

Fix: Add "media_type": "image/jpeg" (or the appropriate MIME type).


Authentication failed. The Authorization header is missing, malformed, or contains an invalid key.

Missing Authorization header

Request:

Terminal window
curl -X POST https://agents.lumalabs.ai/v1/generations \
-H "Content-Type: application/json" \
-d '{"prompt": "A sunset"}'

Response (HTTP 401):

{
"detail": "Missing or invalid API key"
}

Fix: Add -H "Authorization: Bearer $LUMA_AGENTS_API_KEY".

Malformed Authorization header (not Bearer scheme, or wrong key prefix)

The Authorization header must use the Bearer scheme and the token must be a luma-api-* key. If the scheme is wrong or the token doesn’t have the right prefix, the same "Missing or invalid API key" is returned.

Request:

Terminal window
curl -X POST https://agents.lumalabs.ai/v1/generations \
-H "Authorization: $LUMA_AGENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "A sunset"}'

Response (HTTP 401):

{
"detail": "Missing or invalid API key"
}

Fix: Use Authorization: Bearer <key>, and confirm the key starts with luma-api-.

Invalid API key (wrong value, lookup failed)

The Authorization: Bearer ... header is well-formed, but the key value doesn’t match any active key on the server.

Response (HTTP 401):

{
"detail": "Invalid API key"
}

Fix: Check that you copied the full key, and that it hasn’t been replaced. Generate a new one if needed.

Revoked API key

The key was previously valid but has been explicitly revoked from the dashboard.

Request:

Terminal window
curl -X POST https://agents.lumalabs.ai/v1/generations \
-H "Authorization: Bearer luma-api-revoked_key_12345" \
-H "Content-Type: application/json" \
-d '{"prompt": "A sunset"}'

Response (HTTP 401):

{
"detail": "API key has been revoked"
}

Fix: Generate a new API key and update your LUMA_AGENTS_API_KEY environment variable. The old key cannot be un-revoked.

Expired API key

API keys can have an expiration date set when they are created. Once past that date, the key returns 401 even if it has not been revoked.

Response (HTTP 401):

{
"detail": "API key has expired"
}

Fix: Generate a new API key.


Your account does not have enough credits to process this generation.

Response (HTTP 402):

{
"detail": "Insufficient credits. Please add credits to continue."
}

Fix: Add credits to your account from the dashboard, then retry. The request was not queued. See Pricing for current rates.


Access is denied. There are several reasons this can happen:

API client is suspended

Your API client has been suspended, typically due to a policy violation or billing issue.

Response (HTTP 403):

{
"detail": "API client is suspended"
}

Fix: Contact support to resolve the suspension.

API client is deactivated

Your API client has been deactivated.

Response (HTTP 403):

{
"detail": "API client is deactivated"
}

Fix: Contact support to reactivate the client.


An image in your request (source or image_ref) exceeds the 50 MB size limit.

Response (HTTP 413):

{
"detail": "Image exceeds 50 MB size limit"
}

This applies to:

  • The source image in image_edit requests
  • Any entry in the image_ref array
  • Both URL-referenced and base64-encoded images

Fix: Resize or compress the image to under 50 MB before sending.


An image reference was syntactically valid but could not be processed.

Invalid base64 data

The data field contains a string that is not valid base64 encoding.

Request:

{
"prompt": "A sunset",
"image_ref": [
{
"data": "not-valid-base64!!!",
"media_type": "image/png"
}
]
}

Response (HTTP 422):

{
"detail": "image_ref[0]: invalid base64 data"
}

Fix: Ensure the data is properly base64-encoded. In Python: base64.b64encode(image_bytes).decode("utf-8").

URL fetch failure — unreachable host

The URL in url could not be reached.

Request:

{
"prompt": "A sunset",
"image_ref": [
{
"url": "https://nonexistent-domain-12345.com/image.jpg"
}
]
}

Response (HTTP 422):

{
"detail": "image_ref[0]: failed to fetch URL"
}

Fix: Verify the URL is publicly accessible and responds with image content.

URL fetch failure — 404 or other HTTP error

The URL returned an HTTP error when fetched.

Request:

{
"type": "image_edit",
"prompt": "A sunset",
"source": {
"url": "https://example.com/deleted-image.jpg"
}
}

Response (HTTP 422):

{
"detail": "source: failed to fetch URL (HTTP 404)"
}

Fix: Confirm the URL returns a 200 response with valid image content. Test by opening it in a browser or fetching it with curl.

URL fetch failure — not an image

The URL returned successfully but the content is not a valid image.

Request:

{
"prompt": "A sunset",
"image_ref": [
{
"url": "https://example.com/document.pdf"
}
]
}

Response (HTTP 422):

{
"detail": "image_ref[0]: content-type is not an image"
}

Fix: Ensure the URL points to a valid image file (JPEG, PNG, etc.). The detail is surfaced by the upstream fetch proxy, so exact wording may vary.


You’ve hit a rate limit. There are two distinct scenarios:

Requests-per-minute (RPM) limit exceeded

You’ve sent too many requests in the current 60-second sliding window.

Response (HTTP 429):

HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1712592012
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
X-API-Version: 2026-04-01
{
"detail": "Rate limit exceeded"
}

Headers explained:

HeaderExampleMeaning
Retry-After12Seconds until the oldest request in the window ages out (minimum 1)
X-RateLimit-Limit30Your RPM allowance for the rolling 60-second window (example value — yours depends on your plan)
X-RateLimit-Remaining0You have 0 requests remaining
X-RateLimit-Reset1712592012Unix timestamp when the oldest request ages out

Fix: Wait for the duration specified in Retry-After, or until the X-RateLimit-Reset timestamp.

Concurrent job limit exceeded

You have too many generations running simultaneously. Your concurrent-job allowance depends on your plan — check your Luma platform dashboard for the exact ceiling.

Response (HTTP 429):

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-Request-Id: 661f9500-f30c-52e5-b827-557766551111
X-API-Version: 2026-04-01
{
"detail": "Too many concurrent jobs"
}

Note: Concurrent job 429s include a fixed Retry-After: 60 but do not include X-RateLimit-* headers (those only apply to RPM limiting).

Fix: Wait for one or more of your active generations to reach completed or failed before submitting new ones.

Retry strategy for 429 errors:

import random
import time
from luma_agents import RateLimitError
def create_with_retry(client, max_retries=5, **kwargs):
for attempt in range(max_retries):
try:
return client.generations.create(**kwargs)
except RateLimitError as e:
if attempt == max_retries - 1:
raise
# Prefer Retry-After; fall back to exponential backoff with jitter
retry_after = e.response.headers.get("Retry-After")
wait = int(retry_after) if retry_after else 2 ** attempt + random.uniform(0, 1)
print(f"Rate limited. Retrying in {wait:.1f}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)

An upstream service required to process your request is temporarily unavailable.

Response (HTTP 502):

{
"detail": "Upstream service unavailable"
}

This can happen when:

  • The fetch proxy (used to download URL-referenced images) is down
  • Internal scope provisioning fails

Fix: Retry after a brief delay (5–10 seconds). If the error persists, check the status page or contact support.


The image URL ingestion subsystem is not available.

Response (HTTP 503):

{
"detail": "Image URL ingestion unavailable"
}

This specifically affects requests that reference images via url (in source or image_ref).

Fix: As a workaround, download the image yourself and send it as base64 data instead:

import base64
import httpx
# Download the image yourself
response = httpx.get("https://example.com/reference.jpg")
image_b64 = base64.b64encode(response.content).decode("utf-8")
# Send as base64 instead of URL
generation = client.generations.create(
prompt="A similar scene but at sunset",
image_ref=[
{
"data": image_b64,
"media_type": "image/jpeg",
}
],
)

If you don’t need image references, retry without them.


Synchronous errors on GET /v1/generations/{generation_id}

Section titled “Synchronous errors on GET /v1/generations/{generation_id}”

These errors are returned when polling for generation status.

The generation was found and its current status is returned.

Request:

Terminal window
curl https://agents.lumalabs.ai/v1/generations/d290f1ee-6c54-4b01-90e6-d701748f0851 \
-H "Authorization: Bearer $LUMA_AGENTS_API_KEY"

Response (HTTP 200) — still processing:

{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"type": "image",
"state": "processing",
"model": "uni-1",
"created_at": "2026-04-08T12:00:00Z",
"output": [],
"failure_reason": null,
"failure_code": null
}

Response (HTTP 200) — completed:

{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"type": "image",
"state": "completed",
"model": "uni-1",
"created_at": "2026-04-08T12:00:00Z",
"output": [
{
"type": "image",
"url": "https://storage.example.com/generations/d290f1ee/output.png?X-Amz-Expires=3600&..."
}
],
"failure_reason": null,
"failure_code": null
}

Same as the POST endpoint — the Authorization header is missing, malformed, or invalid.

Response (HTTP 401):

{
"detail": "Missing or invalid API key"
}

The generation ID does not exist, or it belongs to a different API key.

Generation does not exist

Request:

Terminal window
curl https://agents.lumalabs.ai/v1/generations/00000000-0000-0000-0000-000000000000 \
-H "Authorization: Bearer $LUMA_AGENTS_API_KEY"

Response (HTTP 404):

{
"detail": "Generation not found"
}

Fix: Verify the generation ID. It should be the id field from the POST /v1/generations response.

Generation belongs to another API key

Generations are scoped to the API key that created them. You cannot access another key’s generations.

Response (HTTP 404):

{
"detail": "Generation not found"
}

The error message is intentionally identical to “does not exist” to prevent enumeration.

Fix: Ensure you’re using the same API key that submitted the original generation.

Invalid generation ID format

The generation_id path parameter must be a valid UUID.

Request:

Terminal window
curl https://agents.lumalabs.ai/v1/generations/not-a-uuid \
-H "Authorization: Bearer $LUMA_AGENTS_API_KEY"

Response (HTTP 404):

{
"detail": "Generation not found"
}

Fix: Use the UUID returned by POST /v1/generations.


These failures happen after the POST succeeds with HTTP 201. You discover them by polling GET /v1/generations/{id} and finding state: "failed".

{
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"type": "image",
"state": "failed",
"model": "uni-1",
"created_at": "2026-04-08T12:00:00Z",
"output": [],
"failure_reason": "Description of what went wrong",
"failure_code": "generation_failed"
}

Key differences from a completed response:

  • state is "failed" instead of "completed"
  • output is an empty array []
  • failure_reason contains a non-null human-readable string
  • failure_code contains a machine-readable code for programmatic handling

The failure_code field provides a machine-readable code you can use in branching logic:

failure_codeDescriptionAction
content_moderatedThe prompt or input image violated content guidelinesModify the prompt and resubmit — do not retry as-is
generation_failedThe model encountered an internal error during generationRetry the same request — this is typically transient
budget_exhaustedThe account ran out of credits partway through the generationAdd credits, then resubmit
output_not_foundThe generated output could not be retrievedRetry the same request

The failure_reason field provides a human-readable description. Common values:

Failure reasonDescriptionAction
Generation output was flagged by our content moderation system.The output image was flagged by content moderationModify the prompt and resubmit
Insufficient credits to complete this generation.The account ran out of credits mid-generationAdd credits, then resubmit
Generation failedThe model encountered an internal error during generationRetry the same request — this is typically transient
Output artifacts not foundThe generated output could not be retrievedRetry the same request

Always check for the failed state when polling. Never assume a generation will complete.

import time
from luma_agents import Luma
client = Luma()
generation = client.generations.create(
prompt="A sunset over the ocean",
)
while generation.state not in ("completed", "failed"):
time.sleep(2)
generation = client.generations.get(generation.id)
if generation.state == "failed":
print(f"Generation {generation.id} failed: {generation.failure_reason}")
# Use failure_code for programmatic branching
if generation.failure_code == "content_moderated":
print("Content policy violation — do not retry with the same prompt")
else:
print("Transient error — safe to retry")
elif generation.state == "completed":
print(f"Success: {generation.output[0].url}")

Output URLs in the output array are presigned S3 URLs that expire after 1 hour. This is not an HTTP error from the API, but a common failure mode in downstream code.

Terminal window
curl -I "https://storage.example.com/generations/d290f1ee/output.png?X-Amz-Expires=3600&..."
HTTP/1.1 403 Forbidden

The S3 presigned URL returns a 403 — the image data is still there, but the signature has expired.

Poll the generation endpoint again. Each call to GET /v1/generations/{id} returns a fresh presigned URL with a new 1-hour expiry:

# Original URL expired
generation = client.generations.get("d290f1ee-6c54-4b01-90e6-d701748f0851")
fresh_url = generation.output[0].url # New presigned URL, valid for 1 hour
  1. Download immediately — Save images to your own storage as soon as the generation completes
  2. Don’t cache URLs — Presigned URLs are ephemeral; cache the downloaded image data instead
  3. Don’t expose URLs to end users — They’ll break after 1 hour; serve images from your own CDN
  4. Re-poll if needed — If you need the image again after expiry, call GET to get a new URL

For the complete guide on rate limiting — including the sliding window algorithm, all response headers, retry strategies, and proactive throttling — see Rate limits and headers.

Quick reference:

Limit429 detail
Requests per minute (RPM)"Rate limit exceeded"
Concurrent jobs"Too many concurrent jobs"

Specific allowances depend on your plan — see your usage dashboard or the X-RateLimit-Limit header on a successful POST.


Include X-Request-Id in your requests to correlate them with responses and support inquiries. The server echoes your value back, or generates a UUID if you don’t provide one. See Rate limits and headers — Request tracing for details.


Use this checklist to verify your integration handles all failure modes:

ScenarioTypeHow to detectAction
Successful generationHappy pathHTTP 201, then poll until state: "completed"Download from output[].url
Validation errorSyncHTTP 400Fix request parameters, do not retry as-is
Auth failureSyncHTTP 401Check API key — missing, malformed, revoked, or expired
No creditsSyncHTTP 402Add credits
Plan restrictionSyncHTTP 403Upgrade plan, change model, or contact support (suspended/deactivated)
Image too largeSyncHTTP 413Compress image to under 50 MB
Bad image dataSyncHTTP 422Fix base64 encoding or image URL
Rate limitedSyncHTTP 429Wait for Retry-After seconds, then retry
Upstream downSyncHTTP 502Retry after 5–10 seconds
Ingestion downSyncHTTP 503Use base64 instead of URL, or retry later
Poll — not foundSyncHTTP 404 on GETCheck generation ID and API key
Async failureAsyncstate: "failed" on pollBranch on failure_code, read failure_reason for details, retry if transient
Expired output URLDownstreamHTTP 403 from S3Re-poll GET to get fresh presigned URL
Generation still runningExpectedstate: "queued" or "processing" on pollContinue polling every 2–5 seconds