> ## Documentation Index
> Fetch the complete documentation index at: https://docs.samsa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Image Uploads

> Upload a local image file to get an image_id you can use as a source in edits, image operations, and image-to-video.

Upload a local image file to Samsa and get an **`image_id`**. Use the `image_id` as
the source image of a [Magic Edit](/api-reference/edits/create.md),
[img2img](/api-reference/image-ops/img2img.md),
[variations](/api-reference/image-ops/variations.md),
[resize](/api-reference/image-ops/resize.md),
[upscale](/api-reference/image-ops/upscale.md),
[background removal](/api-reference/image-ops/remove-background.md),
[vectorization](/api-reference/image-ops/vectorize.md), or an
[image-to-video generation](/api-reference/videos/generate.md). The MCP server has the
same flow as two tools, `prepare_image_upload` and `complete_image_upload` — see
[MCP tools](/mcp-server/tools.md).

Uploads are **free** — they cost no credits.

## The three steps

**1. Prepare**

Call [`POST /images/uploads/prepare`](/api-reference/uploads/prepare) with the
`filename`, the `content_type`, and the **exact** file size in `size_bytes`. The
response gives you the `image_id` and a presigned `upload_url`.

**2. PUT the file**

`PUT` the raw file bytes to `upload_url` with the same `Content-Type`. Do not
send an `Authorization` header to this URL.

**3. Complete**

Call [`POST /images/uploads/{image_id}/complete`](/api-reference/uploads/complete)
within the hour. Samsa verifies the file and returns the image with its measured
`width` and `height`. The `image_id` is usable from now on.

```bash theme={null}
# 1. Prepare: send the EXACT file size
curl -X POST https://api.samsa.ai/public/v1/images/uploads/prepare \
  -H "Authorization: Bearer $SAMSA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"filename\": \"photo.png\", \"content_type\": \"image/png\", \"size_bytes\": $(wc -c < photo.png)}"
# -> {"image_id": "...", "upload_url": "https://...", "expires_at": "...", "max_bytes": 10485760, "allowed_content_types": [...]}

# 2. PUT the raw bytes (same Content-Type, no Authorization header)
curl -X PUT -H "Content-Type: image/png" --data-binary @photo.png "<upload_url>"

# 3. Complete within the hour
curl -X POST https://api.samsa.ai/public/v1/images/uploads/<image_id>/complete \
  -H "Authorization: Bearer $SAMSA_API_KEY"
# -> {"image_id": "...", "status": "completed", "filename": "photo.png", "content_type": "image/png", "size_bytes": 4718592, "width": 2048, "height": 1536}
```

`curl --data-binary @file` sends the real file length, so it always matches
`size_bytes` when you measured the same file.

## Limits

| Rule | Value |
| - | - |
| Accepted formats | `image/jpeg`, `image/png`, `image/webp` |
| Max file size | **10 MB** (10,485,760 bytes) |
| Max pixels | **32 MP** (33,554,432 pixels, width × height) |
| `size_bytes` | Required. The exact file size — it is signed into `upload_url`, so a `PUT` of any other length is rejected |
| `upload_url` validity | **1 hour** — call complete within that hour |
| Uploads awaiting completion | At most **20** per user ([`429 too_many_pending_uploads`](/guides/errors#too_many_pending_uploads) above that) |

The file must be the format you declared in `content_type`. `width` and `height` are
optional hints at prepare: Samsa uses them only to refuse an image over 32 MP early.
The stored dimensions are always measured from the uploaded bytes. An upload that is
never completed is discarded after its URL expires.

## Lifecycle and errors

* **Not usable before complete.** Until complete succeeds, the `image_id` returns
  [`404 not_found`](/guides/errors#not_found) everywhere.
* **Complete is idempotent.** Calling it again returns the same result and changes
  nothing. A later `PUT` to the same `upload_url` does not replace a completed image.
* **`422 validation_error`** — there is no file yet, or the file is empty, has a
  different size than `size_bytes`, is too large, cannot be read, is a different
  format, or is over 32 MP. Samsa discards a refused file: `PUT` a valid file to the
  same `upload_url` while it is still valid, then call complete again.
* **`404 not_found`** — the `image_id` is unknown, expired, belongs to another user,
  or was not created by prepare.
* **[`409 upload_in_progress`](/guides/errors#upload_in_progress)** — another
  complete call for the same upload is running. Retry in a few seconds; if it
  continues for minutes, prepare a new upload.
* **[`429 too_many_pending_uploads`](/guides/errors#too_many_pending_uploads)** —
  you have 20 uploads awaiting completion. Complete them, or wait until unfinished
  ones expire (see `Retry-After`).

## Scope and ownership

Both endpoints accept a key with **any one** of the `images.edit`,
`images.transform`, or `videos.generate` scopes — the scopes of the operations that
take a source `image_id`. There is no separate upload scope, so existing keys work.
A key with none of the three gets
[`403 missing_scope`](/guides/errors#missing_scope).

The uploaded image belongs to the **calling user** — the API key's creator, or the
user of an OAuth token. Other members of your organization cannot use its
`image_id`.

## Provenance

An uploaded image is **not** marked as AI-generated: Samsa did not produce it. An
image that a Samsa operation produces from an upload is an AI-modified output and is
marked like every other edit — see [Content provenance](/guides/content-provenance.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.