---
title: "Upload a file"
method: POST
path: "/files"
tags: ["Files"]
---

# Upload a file

`POST /files`

Upload a file to your namespace, then reference it from a generation via ImageRef.file_id (as source, image_ref[], video.start_frame, keyframes, and so on). Two upload modes share this endpoint, selected by Content-Type:

- multipart/form-data — send the bytes inline in the `file` part. Best for small files (subject to an inline size cap; larger files must use the presigned flow). The returned file is already `pending` ingest.

- application/json — request a presigned upload. The response `upload` envelope tells you where to PUT the bytes; afterward call POST /files/{file_id}/complete to start ingest. Use this for larger files.

## Request body

- CreateFileRequest — JSON body for the presigned upload flow. Returns a PresignedUpload envelope; PUT the bytes, then call POST /files/{file_id}/complete. For small files you can instead POST multipart/form-data with the bytes inline.
  - `filename` string, nullable — Optional original filename to record.
  - `mime_type` string, required — MIME type of the bytes you will upload.
  - `size_bytes` integer, required — Exact size in bytes of the object you will PUT. Up to 5 GiB (the S3 single-PUT ceiling).
  - `purpose` 'input' | 'reference' — How the file is intended to be used in a generation. `input` is the primary subject (e.g. the source image for an edit); `reference` is style/content guidance.
  - `expires_at` string, date-time, nullable — Optional TTL. After this time Luma may automatically delete the file and reclaim its bytes.
  - `user_id` string, nullable — Optional opaque end-user tag for abuse attribution. Mirrors the user_id field on POST /generations.

## Response `201`

File created

- CreateFileResponse — Result of POST /files. In the multipart (inline) flow `upload` is null and the file is already `pending` ingest. In the presigned (JSON) flow `upload` carries the PUT envelope and the file stays `pending` until you call POST /files/{file_id}/complete. Top-level `id` and `state` are conveniences that mirror `file.id` and `file.state`; the full record is always under `file`.
  - `id` string, uuid, required — File identifier.
  - `state` 'pending' | 'ready' | 'failed' | 'deleted', required — Lifecycle state of an uploaded file. `pending` until bytes are received and the ingest pipeline runs; `ready` once it can be referenced from a generation; `failed` if ingest/moderation rejected it; `deleted` after a soft-delete.
  - `upload` PresignedUpload — Where to PUT the file bytes for a presigned (JSON) upload. Issue an HTTP PUT of the raw bytes to `url` with the given `headers`, then call POST /files/{file_id}/complete.
    - `url` string, uri, required — Presigned S3 URL to PUT the bytes to.
    - `method` string, required — HTTP method to use for the upload — always PUT.
    - `headers` object — Headers that must be sent with the PUT request.
    - `expires_at` string, date-time, required — When the presigned URL expires.
  - `file` File, required — A file in the caller's namespace.
    - `id` string, uuid, required — File identifier, referenced as ImageRef.file_id.
    - `filename` string, nullable — Original filename supplied at upload, if any.
    - `mime_type` string, required — MIME type of the stored bytes (for example, image/jpeg).
    - `size_bytes` integer, required — Size of the stored object in bytes.
    - `purpose` 'input' | 'reference', required — How the file is intended to be used in a generation. `input` is the primary subject (e.g. the source image for an edit); `reference` is style/content guidance.
    - `state` 'pending' | 'ready' | 'failed' | 'deleted', required — Lifecycle state of an uploaded file. `pending` until bytes are received and the ingest pipeline runs; `ready` once it can be referenced from a generation; `failed` if ingest/moderation rejected it; `deleted` after a soft-delete.
    - `failure_reason` string, nullable — Human-readable reason when state is failed.
    - `expires_at` string, date-time, nullable — TTL set at upload, if any. After this time Luma may automatically delete the file and reclaim its bytes — you don't need to call DELETE yourself.
    - `created_at` string, date-time, required — Creation timestamp.
    - `deleted_at` string, date-time, nullable — Soft-delete timestamp, if the file was deleted.
    - `user_id` string, nullable — The opaque end-user tag supplied at upload, echoed back unchanged. Abuse-attribution only; not an access-control primitive.

## Other responses

- `401` — Missing or invalid API key
- `413` — File exceeds the inline size cap, or the storage/count quota is exhausted
- `415` — Unsupported Content-Type (must be multipart/form-data or application/json)
- `422` — Invalid request body or form fields
- `429` — Rate limit exceeded
- `503` — File ingest pipeline temporarily unavailable

---

[API](https://skmtc.net/lumalabs/apis/luma-agents-api.md) · [All operations](https://skmtc.net/lumalabs/apis/luma-agents-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/lumalabs/luma-agents-api/versions/2135bbde249c/schema)
