Files
Upload files once with the Luma Agents Files API and reference them across many image and video generations by file_id.
The Files API lets you upload an image or video once and reference it from many generations by its file_id — instead of re-sending a URL or re-encoding base64 on every request. Uploaded files are moderated once at ingest, so referencing one later skips the per-request fetch/decode and the moderation round-trip.
When to use a file
Section titled “When to use a file”A file_id is one of the media reference shapes accepted anywhere ImageRef is — source, image_ref[], video.start_frame / end_frame, and video.keyframes. Use whichever fits:
| You have | Use |
|---|---|
| Media you’ll reference more than once | file_id (upload it) |
| A one-off, publicly reachable URL | url |
| Small bytes you already hold in memory | data (base64) |
| The output of a prior generation | generation_id |
Uploading is worth it when the same asset feeds several generations, when the bytes aren’t publicly hosted, or when you want moderation to happen up front rather than on the generation’s critical path.
Upload a file
Section titled “Upload a file”There are two ways to upload, sharing one endpoint (POST /v1/files). Pick by Content-Type.
Inline (multipart) — small files
Section titled “Inline (multipart) — small files”Send the bytes directly as multipart/form-data. Simplest for small assets; subject to an inline size cap (larger files must use the presigned flow below).
curl -X POST https://agents.lumalabs.ai/v1/files \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -F "file=@./reference.jpg" \ -F "purpose=reference"from luma_agents import Luma
client = Luma()
created = client.files.create( file=open("reference.jpg", "rb"), purpose="reference",)print(created.id, created.state) # -> "<uuid>" "pending"import Luma, { toFile } from "luma-agents";import fs from "node:fs";
const client = new Luma();
const created = await client.files.create({ file: await toFile(fs.createReadStream("reference.jpg")), purpose: "reference",});console.log(created.id, created.state); // -> "<uuid>" "pending"The response is a CreateFileResponse. For an inline upload, upload is null and the file is already pending ingest — no follow-up call needed.
{ "id": "f1e2d3c4-b5a6-7890-abcd-ef0123456789", "state": "pending", "upload": null, "file": { "id": "f1e2d3c4-b5a6-7890-abcd-ef0123456789", "filename": "reference.jpg", "mime_type": "image/jpeg", "size_bytes": 248173, "purpose": "reference", "state": "pending", "created_at": "2026-06-11T12:00:00Z" }}Presigned (JSON) — large files
Section titled “Presigned (JSON) — large files”For larger files, request a presigned upload, PUT the bytes straight to storage, then finalize. Three steps:
1. Request the upload. Declare mime_type and exact size_bytes:
curl -X POST https://agents.lumalabs.ai/v1/files \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "scene.mp4", "mime_type": "video/mp4", "size_bytes": 734003200, "purpose": "input" }'The response carries an upload envelope:
{ "id": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "state": "pending", "upload": { "url": "https://storage.example.com/...&X-Amz-Signature=...", "method": "PUT", "headers": { "Content-Type": "video/mp4" }, "expires_at": "2026-06-11T13:00:00Z" }, "file": { "id": "a1b2c3d4-e5f6-7890-abcd-ef0123456789", "state": "pending", "...": "..." }}2. Upload the bytes with an HTTP PUT to upload.url, sending exactly the upload.headers and the size_bytes you declared:
curl -X PUT "<upload.url>" \ -H "Content-Type: video/mp4" \ --data-binary @./scene.mp43. Complete the upload to kick off ingest:
curl -X POST https://agents.lumalabs.ai/v1/files/a1b2c3d4-e5f6-7890-abcd-ef0123456789/complete \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY"File lifecycle
Section titled “File lifecycle”After upload, ingest and moderation run asynchronously. Poll GET /v1/files/{file_id} until the file reaches a terminal state:
| State | Meaning |
|---|---|
pending | Bytes received; ingest/moderation in progress. Not yet referenceable. |
ready | Available — reference it from a generation by file_id. |
failed | Ingest or moderation rejected the file; see failure_reason. |
deleted | Soft-deleted; can no longer be referenced. |
A file must be ready before a generation can reference it. Referencing a pending, failed, or deleted file is rejected.
Reference a file in a generation
Section titled “Reference a file in a generation”Once a file is ready, pass its id as file_id anywhere an ImageRef is accepted. The shape is interchangeable with url / data / generation_id — provide exactly one.
As the edit source:
generation = client.generations.create( type="image_edit", prompt="Change the sky to a dramatic sunset", source={"file_id": "f1e2d3c4-b5a6-7890-abcd-ef0123456789"},)const generation = await client.generations.create({ type: "image_edit", prompt: "Change the sky to a dramatic sunset", source: { file_id: "f1e2d3c4-b5a6-7890-abcd-ef0123456789" },});curl -X POST https://agents.lumalabs.ai/v1/generations \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "image_edit", "prompt": "Change the sky to a dramatic sunset", "source": { "file_id": "f1e2d3c4-b5a6-7890-abcd-ef0123456789" } }'As a reference image (and the same form works for video.start_frame, video.end_frame, and video.keyframes[]):
{ "type": "image", "prompt": "A product photo in the same lighting as the reference", "image_ref": [{ "file_id": "f1e2d3c4-b5a6-7890-abcd-ef0123456789" }]}List, get, and delete
Section titled “List, get, and delete”List your files, newest first (keyset-paginated — pass next_cursor back as cursor):
curl "https://agents.lumalabs.ai/v1/files?limit=25&purpose=reference" \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY"page = client.files.list(limit=25, purpose="reference")for f in page.data: print(f.id, f.state)if page.has_more: next_page = client.files.list(cursor=page.next_cursor)const page = await client.files.list({ limit: 25, purpose: "reference" });for (const f of page.data) console.log(f.id, f.state);Get a single file’s metadata:
curl https://agents.lumalabs.ai/v1/files/f1e2d3c4-b5a6-7890-abcd-ef0123456789 \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY"Delete (soft-delete — the file can no longer be referenced; returns 204):
curl -X DELETE https://agents.lumalabs.ai/v1/files/f1e2d3c4-b5a6-7890-abcd-ef0123456789 \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY"Fields
Section titled “Fields”| Field | Notes |
|---|---|
purpose | input (a primary subject, e.g. an edit source) or reference (style/content guidance). Defaults to input. |
mime_type | Required in the presigned flow; sniffed from the bytes in the inline flow. |
size_bytes | Required (and enforced) in the presigned flow. Up to 5 GiB (the storage single-PUT ceiling). |
expires_at | Optional TTL; the file becomes eligible for deletion afterward. |
user_id | Optional opaque end-user tag for abuse attribution — mirrors user_id on generations. Echoed back unchanged. |
Common errors
Section titled “Common errors”| HTTP | When |
|---|---|
| 401 | Missing or invalid API key |
| 404 | The file isn’t yours, or doesn’t exist |
| 413 | Inline upload over the size cap, or your storage / file-count quota is full |
| 415 | Content-Type is neither multipart/form-data nor application/json |
| 422 | Invalid body or form fields; on complete, the bytes haven’t been PUT yet |
| 429 | Rate limit exceeded |
| 503 | File ingest pipeline temporarily unavailable — retry shortly |
Next steps
Section titled “Next steps”- Image editing — Reference an uploaded
file_idas the editsource - Image generation — Use uploaded files as
image_ref - Video generation — Use uploaded files as
start_frame/end_frame/keyframes - API Reference — Complete
filesendpoint specifications - Error handling — Every error code with troubleshooting steps