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 balanceNo — add funds
403Access denied (suspended or deactivated client)No — contact support
413Input media exceeds size limitNo — resize media
422Parameter combination rejected, or bad media data (base64 or URL)No — fix request
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_exhaustedRan out of funds mid-generationNo — add funds, then retry
output_not_foundGenerated output could not be retrievedYes — retry same request
image_too_largeAn input image exceeded the size limit (detected during processing)No — resize/compress the input
unsupported_formatAn input media file was in an unsupported formatNo — convert to a supported format
corrupt_inputAn input media file could not be decodedNo — re-encode or replace the input
invalid_requestThe request was rejected as invalid during generationNo — fix the request parameters
rate_limitedAn upstream provider rate-limited the generationYes — retry with backoff
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, uni-1-max, and ray-3.2 — all available to all accounts. See Models for the capability matrix. 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 for this model

Each model accepts a specific set of type values. Crossing model and type returns HTTP 400 with the exact pair in the detail.

Example — type: "video" on an image model:

{ "prompt": "A sunset over the ocean", "type": "video" }
{ "detail": "Type 'video' is not supported for model 'uni-1'" }

Example — type: "image" on a video model:

{ "model": "ray-3.2", "type": "image", "prompt": "A sunset over the ocean" }
{ "detail": "Type 'image' is not supported for model 'ray-3.2'" }

Fix: Use a type value the model supports. uni-1 / uni-1-max accept image and image_edit. ray-3.2 accepts video, video_edit, and video_reframe. See Models for the full capability matrix.

source missing on video_edit or video_reframe

source is required for type: "video_edit" and type: "video_reframe". Provide a prior generation id as source.generation_id, a hosted video URL as source.url, or inline base64 video as source.data — the last two paired with a video/* source.media_type.

Request:

{
"model": "ray-3.2",
"type": "video_edit",
"prompt": "Transform the scene into moonlit film footage"
}

Response (HTTP 400):

{
"detail": "source is required for type 'video_edit' or 'video_reframe' — provide source.generation_id (UUID of a prior generation) or source.url / source.data with source.media_type='video/mp4'"
}

Fix: Pass the top-level id of a prior completed video generation as source.generation_id, or pass a hosted video as source.url / inline video as source.data with source.media_type: "video/mp4". See Video editing — source video.

source.generation_id set on the wrong request type

source.generation_id chains off a prior generation’s output — valid for type: "image_edit" (prior image), type: "video_edit", and type: "video_reframe" (prior video). It is rejected on type: "video". For video extend, put the prior generation id in video.start_frame.generation_id or video.end_frame.generation_id instead.

Request:

{
"model": "ray-3.2",
"type": "video",
"prompt": "A sunset over the ocean",
"source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" }
}

Response (HTTP 400):

{
"detail": "source.generation_id is only valid for type 'image_edit', 'video_edit', or 'video_reframe'"
}

Fix: Remove source, change type to "video_edit" / "video_reframe", or move the generation reference to a video keyframe for extend.

source.url or source.data on video_edit / video_reframe without a video media_type

A hosted source.url or inline source.data video is supported for type: "video_edit" and type: "video_reframe", but you must declare a video/* source.media_type so the route can dispatch video ingest before fetching any bytes. A missing or non-video/* value is rejected.

Request:

{
"model": "ray-3.2",
"type": "video_edit",
"prompt": "winter version",
"source": { "url": "https://example.com/source.mp4" }
}

Response (HTTP 400):

{
"detail": "source.media_type must be a video/* type (e.g. 'video/mp4') when providing source.url / source.data on type='video_edit'"
}

Fix: Add source.media_type, for example "video/mp4". Source videos must be 30 seconds or shorter. See Video editing — source video.

image_ref set on video_edit

image_ref is image-only — video edits have no equivalent input channel. Setting it on video_edit is rejected.

Request:

{
"model": "ray-3.2",
"type": "video_edit",
"prompt": "winter version",
"source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" },
"image_ref": [{ "url": "https://example.com/style.jpg" }]
}

Response (HTTP 400):

{
"detail": "image_ref is not supported for type 'video_edit'"
}

Fix: Remove image_ref. Guide the edit through the prompt and video.edit (strength, auto_controls, or per-signal controls).

video.loop on a video_edit request

loop is create-only — valid for type: "video" only.

Request:

{
"model": "ray-3.2",
"type": "video_edit",
"prompt": "Transform the scene",
"source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" },
"video": { "loop": true }
}

Response (HTTP 400):

{
"detail": "video.loop is only valid for type 'video'"
}

Fix: Drop loop, or switch to type: "video".

video.edit on a video (create) request

The video.edit conditioning block is edit-only — valid for type: "video_edit" only.

Request:

{
"model": "ray-3.2",
"type": "video",
"prompt": "A sunset over the ocean",
"video": { "edit": { "strength": "flex_2" } }
}

Response (HTTP 400):

{
"detail": "video.edit is only valid for type 'video_edit'"
}

Fix: Drop video.edit for generation, or set type: "video_edit" with a valid source video. See Video editing — edit controls.

video.edit.auto_controls: true combined with video.edit.controls

Auto mode derives the conditioning schedule from the source video, so it can’t be combined with manual per-signal controls. Pick one.

Request:

{
"model": "ray-3.2",
"type": "video_edit",
"prompt": "Transform the scene",
"source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" },
"video": {
"edit": {
"auto_controls": true,
"controls": { "pose": { "enabled": true } }
}
}
}

Response (HTTP 400):

{
"detail": "video.edit.auto_controls=true cannot be combined with video.edit.controls (auto mode derives the schedule and ignores manual per-signal controls)"
}

Fix: Use auto_controls: true for a model-derived schedule, or drop auto_controls and supply strength / controls for manual mode. See Video editing — edit controls.

Invalid video.edit.keyframes / keyframe_indexes

video.edit.keyframes (guide-frame images) and video.edit.keyframe_indexes (their source-frame positions) are parallel arrays — each keyframes[i] is anchored at keyframe_indexes[i]. Several constraints are enforced, each returning HTTP 400:

Mistakedetail
Only one of the two arrays providedvideo.edit.keyframes and video.edit.keyframe_indexes must both be provided or both omitted
Arrays of different lengthsvideo.edit.keyframe_indexes must have the same length as video.edit.keyframes
A negative frame indexvideo.edit.keyframe_indexes must be non-negative
Duplicate frame indexesvideo.edit.keyframe_indexes must be unique

Fix: Provide both arrays at equal length, with non-negative, unique indexes. See Video editing — guide frames.

video.start_frame combined with video.edit.keyframes

A single video.start_frame and the multi-anchor video.edit.keyframes spell the same intent two ways (start_frame is equivalent to keyframes=[X] at keyframe_indexes=[0]), so they’re mutually exclusive. Setting both is rejected rather than silently merged.

Response (HTTP 400):

{
"detail": "video.start_frame and video.edit.keyframes are mutually exclusive — use video.start_frame for the single-anchor case (frame at index 0), or video.edit.keyframes + keyframe_indexes for multi-anchor / arbitrary positions"
}

Fix: Pick one — video.start_frame for a single anchor, or video.edit.keyframes + keyframe_indexes for multiple. See Video editing — guide frames.

video.source_position set on a non-reframe type

video.source_position controls where the source video sits inside the new canvas and is only meaningful when reframing.

Response (HTTP 400):

{
"detail": "video.source_position is only valid for type 'video_reframe'"
}

Fix: Drop video.source_position, or set type: "video_reframe" with a target aspect_ratio. See Video reframing — video.source_position.

Incompatible video option combinations

Some video options can’t be combined. Each combination is rejected with HTTP 400 and a specific detail:

{ "detail": "resolution '540p' is not supported with hdr=true" }
{ "detail": "duration '10s' is not supported with hdr=true" }
{ "detail": "duration '10s' is not supported with start_frame / end_frame" }
{ "detail": "loop is not supported with duration '10s'" }
{ "detail": "loop is not supported with hdr=true" }
{
"detail": "loop is not supported with end_frame (no seamless start↔end match)"
}
{ "detail": "exr_export requires hdr=true" }

Fix: Adjust the request so the options are compatible — HDR needs 720p/1080p and 5s; exr_export needs hdr: true; loop needs 5s, standard dynamic range, and no end_frame; 10s can’t be combined with anchor frames or HDR. See Video generation — parameters.

Video extend constraints

A type: "video" request with exactly one generation_id keyframe is a video extend request. Extend rejects HDR, and looping is only supported for forward extend.

{ "detail": "HDR is not supported with video extend" }
{ "detail": "loop is only supported with forward video extend" }
{
"detail": "generation-ref keyframes are only supported for video extend (a single start_frame OR end_frame generation reference); interpolate-from-generation is not yet available"
}

Fix: Use exactly one generation_id keyframe, drop HDR/EXR, and set loop: true only when the generation_id is in video.start_frame.

Video reframe-only constraints

type: "video_reframe" requires a target aspect_ratio and rejects controls that do not apply to reframe.

{
"detail": "aspect_ratio (the target shape) is required for type 'video_reframe'"
}
{
"detail": "video.start_frame / video.end_frame are not supported for type 'video_reframe'"
}
{ "detail": "video.loop is not supported for type 'video_reframe'" }
{ "detail": "HDR / exr_export are not supported for type 'video_reframe'" }
{ "detail": "video.edit is not supported for type 'video_reframe'" }

1080p reframe works for landscape and square aspect ratios. Only vertical targets (9:16, 3:4) at 1080p are not yet available — those return:

{
"detail": "1080p resolution is not yet available for vertical reframe (aspect_ratio '9:16', type 'video_reframe') — coming soon. Use a landscape aspect_ratio at 1080p, or resolution '720p' / '540p'."
}

Fix: Provide aspect_ratio, keep video to resolution (and optional source_position) only, and reference a valid source video. For a vertical target, use 720p / 540p instead of 1080p. See Video reframing.

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 supported aspect ratios. The Valid: list is sorted lexicographically by the server and is model-and-type-dependent — it enumerates exactly what is accepted for the model and type you sent. Image models accept the nine ratios listed above; Ray 3.2 video accepts 9:16, 3:4, 1:1, 4:3, 16:9, 21:9. See Image generation — aspect_ratio and Video generation — aspect_ratio.

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', 'video_edit' / 'video_reframe' with source.generation_id, or 'video_edit' / 'video_reframe' with a video source.url / source.data"
}

Fix: Remove the source field, change type to "image_edit", or use the video source contract on type: "video_edit" / "video_reframe".

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 funds to process this generation.

Response (HTTP 402):

{
"detail": "Insufficient balance. Please add funds to continue."
}

Fix: Add funds 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 input media reference in your request exceeds the size limit.

Response (HTTP 413): the detail names the offending field and the limit it crossed — either the 50 MB byte cap or the per-side pixel cap:

{ "detail": "source: image exceeds 50 MB limit" }
{ "detail": "image_ref[2]: image exceeds 50 MB limit" }
{ "detail": "source: image is 9000x6000px but each side must be <= 8000px. Resize and try again." }

This applies to:

  • The source image in image_edit requests
  • Any entry in the image_ref array
  • The video.start_frame and video.end_frame anchor images on Ray 3.2 video requests
  • Inline source.data video on video_edit or video_reframe
  • Both URL-referenced and base64-encoded media

source.generation_id references an existing Luma generation by ID — the request body itself does not transport video bytes, so there is no 413 path at submit time for that source shape.

Fix: Resize or compress the input media before sending. Inline source videos for edit and reframe must also be 30 seconds or shorter.


The request was syntactically valid (well-formed JSON, every field has the right type) but the body violates a semantic rule. There are two distinct families:

  1. Parameter-combination rejections — a value is allowed in isolation but not in combination with another field on the same request. Today the only such rule is style: "manga" + non-portrait aspect_ratio (see below). More may be added as new style presets ship; the response shape is identical.
  2. Media-reference processing failuressource, an image_ref entry, or a video.start_frame / video.end_frame reference is well-formed but couldn’t be loaded (bad base64, unreachable URL, wrong content-type).
Manga style with a non-portrait aspect ratio

style: "manga" is portrait-only. Pairing it with a landscape or square aspect_ratio (3:1, 2:1, 16:9, 3:2, 1:1) is rejected with HTTP 422; the detail includes the list of valid portrait ratios so you can correct the request.

Request:

{
"prompt": "A warrior standing at the edge of a cliff",
"style": "manga",
"aspect_ratio": "1:1"
}

Response (HTTP 422):

{
"detail": "aspect_ratio='1:1' is not allowed when style='manga'. Valid aspect_ratio: 1:2, 1:3, 2:3, 9:16"
}

The detail format is:

aspect_ratio='<value-you-sent>' is not allowed when style='manga'. Valid aspect_ratio: 1:2, 1:3, 2:3, 9:16

The Valid aspect_ratio: list is sorted lexicographically and enumerates the four portrait ratios accepted with style: "manga"2:3, 9:16, 1:2, 1:3. Branch on the 422 status code; treat the detail string as human-readable rather than parsing it programmatically.

Fix: Use one of the portrait ratios listed in the response, or omit aspect_ratio and let the model pick one.

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): the detail names the field whose fetch failed:

{ "detail": "source: fetch proxy unavailable" }

This happens when the fetch proxy (used to download URL-referenced media) is temporarily down.

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": "Media URL ingestion is unavailable (fetch proxy not configured)"
}

This specifically affects requests that reference media 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 funds partway through the generationAdd funds, then resubmit
output_not_foundThe generated output could not be retrievedRetry the same request
image_too_largeAn input image exceeded the size limit, detected during processing rather than at submitResize or compress the input image, then resubmit — do not retry as-is
unsupported_formatAn input media file was in a format the model can’t processConvert the input to a supported format, then resubmit
corrupt_inputAn input media file could not be decodedRe-encode or replace the input, then resubmit
invalid_requestThe request was rejected as invalid during generationFix the request parameters — do not retry as-is
rate_limitedAn upstream provider rate-limited the generationRetry the same request with exponential backoff

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 balance to complete this generation.The account ran out of funds mid-generationAdd funds, 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 fundsSyncHTTP 402Add funds
Plan restrictionSyncHTTP 403Upgrade plan, change model, or contact support (suspended/deactivated)
Input media too largeSyncHTTP 413Compress image to under 50 MB
Incompatible parameters or bad media dataSyncHTTP 422Fix the parameter combination (e.g. style+aspect_ratio) or fix the base64/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