# impartire API — Reference: Uploads

Every path is under `https://impartire.com/api/v1`. The machine-readable contract is https://impartire.com/api/v1/openapi.json (OpenAPI 3.1).

#### POST /api/v1/uploads

Presign a single PUT, or start a multipart upload, for a file (step 1 of creating an asset with a file)

PUT the file's bytes to `upload_url` with `headers`, then create the asset with `file.upload_id` (or attach it to an existing asset with POST /assets/{id}/files). A single PUT carries up to 5 GiB. With `multipart: true` (required above 5 GiB, allowed from 5 MiB, up to 5 TiB) the answer has `method: MULTIPART`, no `upload_url` or `headers`, and `multipart`: the server-chosen `part_size_bytes` and `part_count`. Sign the parts' URLs with POST /uploads/{id}/parts, PUT each part, then POST /uploads/{id}/complete.

Scopes: `assets:write`

Parameters:

- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `CreateUploadRequest`

- `filename` (string, required) — The file's name; its extension is what slots match on.
- `content_type` (string, required) — The MIME type the PUT will send.
- `size_bytes` (integer, required) — The exact size of the file. Required: a single PUT's URL is bound to it, and a multipart upload is checked against it when completed. At most 5 GiB for a single PUT, 5 TiB with `multipart`.
- `multipart` (boolean) — Upload in parts (required above 5 GiB, allowed from 5 MiB). The response's `multipart` gives the part size and count; sign the parts' URLs with POST /api/v1/uploads/{id}/parts, PUT each part, then POST /api/v1/uploads/{id}/complete. Default false: one PUT.

Response 201 — `Upload`

- `upload_id` (string (uuid), required)
- `upload_url` (string or null, required) — PUT the file's bytes here, with `headers`. Null for a multipart upload: its parts' URLs come from `multipart.parts_url`.
- `method` ("PUT" | "MULTIPART", required) — PUT: one PUT to upload_url. MULTIPART: parts, see `multipart`.
- `headers` (map of string or null, required) — Headers the PUT must send. Null for a multipart upload (a part's PUT needs none).
- `url_expires_at` (string (date-time) or null, required) — When upload_url stops accepting a new PUT (15 minutes). Null for a multipart upload: each part URL carries its own expiry.
- `expires_at` (string (date-time), required) — When the upload must be used by (24 hours).
- `max_bytes` (integer, required)
- `status` ("pending", required)
- `multipart` (UploadMultipart or null, required) — Set for a multipart upload; null for a single PUT.
  - `part_size_bytes` (integer, required) — Every part is exactly this size except the last, which is the remainder. Chosen by the server: at least 8 MiB, a whole number of MiB, at most 10 000 parts.
  - `part_count` (integer, required) — How many parts: ceil(size_bytes / part_size_bytes). Part numbers run 1..part_count; part n is bytes (n-1)·part_size_bytes onward.
  - `parts_url` (string, required) — Where to sign the parts' upload URLs (POST, `{ part_numbers }`).

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 409 (Problem) — idempotency-in-flight.
- 422 (Problem) — validation (e.g. size_bytes over 5 GiB without `multipart`, `multipart` under 5 MiB), or idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — storage-unavailable.

#### GET /api/v1/uploads/{id}

Get an upload's status: pending, landed, consumed or expired

Scopes: `assets:write`

Parameters:

- `id` (path, string (uuid), required) — upload_id from POST /uploads.

Response 200 — `UploadStatus`

- `upload_id` (string (uuid), required)
- `status` ("pending" | "landed" | "consumed" | "expired", required) — pending: no bytes yet (for a multipart upload: not completed yet). landed: the bytes are in storage, ready for POST /api/v1/assets or POST /api/v1/assets/{id}/files. consumed: an asset was created from it, or it was attached to one (asset_id). expired: past expires_at, or aborted.
- `size_bytes` (integer or null, required)
- `asset_id` (string (uuid) or null, required) — Set when consumed.
- `expires_at` (string (date-time), required)
- `multipart` (UploadMultipart or null, required) — Set for a multipart upload; null for a single PUT.
  - `part_size_bytes` (integer, required) — Every part is exactly this size except the last, which is the remainder. Chosen by the server: at least 8 MiB, a whole number of MiB, at most 10 000 parts.
  - `part_count` (integer, required) — How many parts: ceil(size_bytes / part_size_bytes). Part numbers run 1..part_count; part n is bytes (n-1)·part_size_bytes onward.
  - `parts_url` (string, required) — Where to sign the parts' upload URLs (POST, `{ part_numbers }`).

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — storage-unavailable.

#### POST /api/v1/uploads/{id}/parts

Sign upload URLs for parts of a multipart upload

Up to 100 distinct part numbers per request, each from 1 to `multipart.part_count`. PUT part n's bytes (offset (n-1)·`part_size_bytes`, `part_size_bytes` long, the last part the rest) to its URL within 15 minutes and keep the PUT's `ETag` header for …/complete. Signing writes nothing, so asking again just signs fresh URLs: no Idempotency-Key.

Scopes: `assets:write`

Parameters:

- `id` (path, string (uuid), required) — upload_id from POST /uploads with `multipart: true`.

Request body — `SignPartsRequest`

- `part_numbers` (array of integer, required) — The parts to sign: 1 to 100 distinct part numbers, each from 1 to the upload's `multipart.part_count`. Sign in batches for more.

Response 200 — `UploadPartUrlList`

- `data` (array of UploadPartUrl, required) — One per requested part number, in the order asked.
  - `part_number` (integer, required)
  - `url` (string, required) — PUT exactly this part's bytes here, with no extra headers; keep the response's ETag header for POST …/complete.
  - `expires_at` (string (date-time), required) — When url stops accepting a new PUT (15 minutes). Sign the part again for a fresh one.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found — not this credential's upload, or no longer in progress (completed, aborted, used or expired).
- 422 (Problem) — validation (part_numbers), or not-multipart — the upload is a single PUT.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — storage-unavailable.

#### POST /api/v1/uploads/{id}/complete

Complete a multipart upload from its parts

Every part once, in any order, each with the ETag its PUT answered. The assembled file must be `size_bytes` long: otherwise it is discarded, the upload fails, and the answer is 422 `size-mismatch`. On success the upload is `landed`, ready for POST /assets or POST /assets/{id}/files. Completing an upload already completed answers its status again.

Scopes: `assets:write`

Parameters:

- `id` (path, string (uuid), required) — upload_id from POST /uploads with `multipart: true`.
- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `CompleteUploadRequest`

- `parts` (array of object, required) — Every part, once each: all `multipart.part_count` of them, in any order.
  - `part_number` (integer, required)
  - `etag` (string, required) — The ETag header the part's PUT answered with, quotes included or not.

Response 200 — `UploadStatus`

- `upload_id` (string (uuid), required)
- `status` ("pending" | "landed" | "consumed" | "expired", required) — pending: no bytes yet (for a multipart upload: not completed yet). landed: the bytes are in storage, ready for POST /api/v1/assets or POST /api/v1/assets/{id}/files. consumed: an asset was created from it, or it was attached to one (asset_id). expired: past expires_at, or aborted.
- `size_bytes` (integer or null, required)
- `asset_id` (string (uuid) or null, required) — Set when consumed.
- `expires_at` (string (date-time), required)
- `multipart` (UploadMultipart or null, required) — Set for a multipart upload; null for a single PUT.
  - `part_size_bytes` (integer, required) — Every part is exactly this size except the last, which is the remainder. Chosen by the server: at least 8 MiB, a whole number of MiB, at most 10 000 parts.
  - `part_count` (integer, required) — How many parts: ceil(size_bytes / part_size_bytes). Part numbers run 1..part_count; part n is bytes (n-1)·part_size_bytes onward.
  - `parts_url` (string, required) — Where to sign the parts' upload URLs (POST, `{ part_numbers }`).

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found — not this credential's upload, or aborted, failed or expired.
- 409 (Problem) — idempotency-in-flight.
- 422 (Problem) — validation (parts — missing, repeated, or refused by storage), not-multipart, size-mismatch, or idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — storage-unavailable.

#### POST /api/v1/uploads/{id}/abort

Abort a multipart upload in progress

Frees the parts stored so far; the upload reads `expired`. Aborting again, or after completing, is 404.

Scopes: `assets:write`

Parameters:

- `id` (path, string (uuid), required) — upload_id from POST /uploads with `multipart: true`.

Other responses:

- 204 — Aborted.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found — not this credential's upload, or not in progress (completed, aborted, used).
- 422 (Problem) — not-multipart — the upload is a single PUT.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — storage-unavailable.
