---
title: "Upload one image"
method: POST
path: "/api/v1/assets/upload"
tags: ["Asset Management"]
---

# Upload one image

`POST /api/v1/assets/upload`

## Request

- Base URL: `https://api.imagestep.dev`
- URL: `https://api.imagestep.dev/api/v1/assets/upload`
- Auth: API key in header `Authorization`

Stores the request body as an asset in one call: the bytes are the body, the parameters are the
query string. Send the image as the raw body with its own `Content-Type` (`image/jpeg`, `image/png`, …)
and a `Content-Length`, or as `multipart/form-data` with a `file` part. The asset is created in
PROCESSING and reaches DONE once ingest has written dimensions and metadata — poll
`POST /api/v1/assets/status` or subscribe to the webhook.

The type is the declared `Content-Type` when it is an image type this product takes, else the
extension of `name` (or the multipart file name); neither → `400 unsupported_format`. Bytes the
account already holds as a DONE asset (same SHA-1) come back as that asset with `existing: true`
and nothing new is stored.

LIMITS: one file per call, at most 25 MB (`413 payload_too_large`); a raw body needs
`Content-Length` (`400 invalid_param`). Over this node's concurrent-upload capacity →
`503 provider_unavailable` with `Retry-After`, retryable. No `Idempotency-Key` (contract §3):
the same bytes are the same asset, so re-sending is already safe.

USE THIS WHEN:
- You hold one image of up to 25 MB and want an `asset_id` for a job

DO NOT USE WHEN:
- The file is over 25 MB, or you have many → `POST /api/v1/assets/stage-upload`, PUT, then finish-upload
  (the bytes then go straight to the object store)
- The image is at a public URL → `POST /api/v1/assets/from-url`
- You only want one image transformed and back → `POST /api/v1/images/transform`

## Query parameters

- `name` string
- `collection` string
- `tags` string[]
- `retentionDays` integer

## Response `200`

The asset: new (PROCESSING), or the DONE one you already hold for the same bytes (`existing: true`)

## Other responses

- `400`: - `invalid_param` on `Content-Length` — a raw body sent chunked, with no length - `invalid_param` on `body` — an empty body - `invalid_param` on `file` — a multipart request with no `file` part - `invalid_param` — a JSON body; to ingest by URL use POST /api/v1/assets/from-url (`details.supported` lists what this takes) - `invalid_param` on `collection` or `tags` — over a limit, a control character, or a collection starting with `job:` - `unsupported_format` — neither the Content-Type nor the name's extension is an image type this service takes
- `413`: `payload_too_large` on `file` — over the 25 MB this endpoint takes; `details.limit` is the ceiling in bytes. Larger files take the three-step upload
- `415`: `unsupported_media_type` — a form-encoded body, which is consumed before this endpoint can read it; `details.supported` lists what it takes
- `422`: `asset_count_exceeded` — your plan's asset limit is reached
- `429`: Rate limited — `error.code` is `rate_limited` and `retryable` is true. `Retry-After` says how many seconds to wait.
- `503`: `provider_unavailable` — this node is at its concurrent-upload capacity (`details.reason` is `capacity`). Retryable, after `Retry-After` (2 s)

---

[API](https://skmtc.dev/imagestep/apis/imagestep-api.md) · [All operations](https://skmtc.dev/imagestep/apis/imagestep-api/llms.txt) · [OpenAPI document](https://skmtc.dev/imagestep/apis/imagestep-api/revisions/5e8e922a3085?raw)
