---
title: "Direct upload — send a whole file in one request"
method: POST
path: "/api/upload"
tags: ["Media Upload - Direct"]
---

# Direct upload — send a whole file in one request

`POST /api/upload`

## Request

- Base URL: `https://snapwonders.com`
- URL: `https://snapwonders.com/api/upload`
- Auth: API key in header `X-Api-Key`

Uploads a file into an existing upload session in a single POST, with the file as the raw request body.

**This is the recommended method for AI agents, scripts and any caller sending a single file under the size limit.** It needs no protocol support: one request, plain headers, no base64 metadata and no byte offsets.

The alternative is the **resumable** method (`/api/tus`, TUS protocol), which is what the website and mobile apps use. Choose resumable when the file exceeds the size limit below, when the connection is unreliable (mobile, Tor/I2P), or when you need a progress bar.

**Order of operations:** create a session (`POST /api/analyse/session`, `/api/convert/session`, or `/api/session`) → upload here → start the job. Check `ready_for_job_start` in the response rather than polling the session-files endpoint. A steganography *hide* needs two uploads — the payload (`X-Upload-Step: 1`) and the cover media (`X-Upload-Step: 2`) — so it only reports ready once both have arrived.

**`Content-Type` must be set.** Without it `curl --data-binary` sends `application/x-www-form-urlencoded` and the body is parsed as form data rather than a file.

**`Content-Length` is required**; chunked transfer-encoding is refused. Without a declared length a body truncated in transit cannot be distinguished from a complete one, and analysing a truncated file would return results describing a file you never sent.

**Retries:** send the same `X-Client-Upload-Id` to make a retry safe. If the original succeeded but its response was lost, the retry returns that original result (HTTP 200, `deduplicated: true`) instead of uploading a second copy. Without the header a retry creates a second file.

## Headers

- `X-Api-Key` string, required
- `X-Upload-Uid` string, required
- `X-Filename` string
- `X-Upload-Step` integer
- `X-Client-Upload-Id` string
- `Content-Type` string, required
- `Content-Length` integer, required

## Response `200`

Replay of a previous upload with the same `X-Client-Upload-Id` — `deduplicated` is true and the original file is returned. Nothing was uploaded twice.

## Other responses

- `201`: Uploaded and stored. Returns the file record, the session status, and `ready_for_job_start`.
- `400`: `truncated_body` — the bytes received did not match Content-Length, so the file was rejected unprocessed. Also `empty_body`.
- `401`: Not authenticated. `error` names the exact cause: `missing_api_key`, `malformed_api_key`, `invalid_api_key`, or — when the key itself is valid — `account_not_verified` / `account_inactive`.
- `403`: `forbidden` — the session exists but belongs to a different API key.
- `404`: `unknown_session` — no session matches X-Upload-Uid.
- `409`: `session_not_accepting_uploads` — the session's job has already been started.
- `410`: `session_expired` — sessions are valid for 24 hours from creation.
- `411`: `length_required` — Content-Length is missing (chunked transfer-encoding). Send a fixed-length body or use the resumable endpoint.
- `413`: `file_too_large` — over the direct-upload limit. The response carries `max_bytes` and `tus_endpoint` so the caller can switch to the resumable method.

---

[API](https://skmtc.dev/snapwonders/apis/snapwonders-api-mcp.md) · [All operations](https://skmtc.dev/snapwonders/apis/snapwonders-api-mcp/llms.txt) · [OpenAPI document](https://skmtc.dev/snapwonders/apis/snapwonders-api-mcp/revisions/645bbccc9550?raw)
