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

# Uploads

> Presigned uploads for audio, image, and document files

Files are never sent through the API directly. You request a presigned URL, then `PUT` the file straight to storage. This keeps large uploads fast and off the API's request path.

## Flow

1. `POST /v1/upload` with `{ filename, size }` (size in bytes).
2. Receive `{ id, uploadUrl, contentType, uploadUrlExpiresIn, expiresAt }`.
3. `PUT` the raw file bytes to `uploadUrl` with **exactly** the returned `Content-Type`; the presigned signature is bound to it.
4. Use `id` as `audioId` (audio jobs), `imageId` (image jobs), or `fileId` (text jobs).

```bash theme={null}
# 1. Request URL
curl -s https://api.robinzip.app/v1/upload \
  -H "Authorization: Bearer $ROBIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename": "notes.pdf", "size": 1048576}'

# 2. PUT the file
curl -s -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @notes.pdf
```

## Constraints

| Constraint          | Value                                                            |
| ------------------- | ---------------------------------------------------------------- |
| Allowed formats     | `.mp3`, `.wav`, `.pdf`, `.txt`, `.jpg`, `.jpeg`, `.png`, `.webp` |
| Max file size       | Plan-dependent: 25 MB (Free), 100 MB (Pro)                       |
| Upload URL validity | 15 minutes (`uploadUrlExpiresIn: 900`)                           |
| Upload lifetime     | Must be consumed by a job within **24 hours**                    |

Content types are assigned by extension: `audio/mpeg`, `audio/wav`, `application/pdf`, `text/plain`, `image/jpeg`, `image/png`, `image/webp`.

## Validation

The declared `size` is checked against your plan limit when the URL is created. When the upload is first consumed by a job, the API additionally verifies that:

* the object actually landed in storage (`UPLOAD_NOT_COMPLETED`, 409, if the `PUT` never happened),
* the real size is within your plan limit (`FILE_TOO_LARGE`, 413),
* the file's magic bytes match a valid MP3/WAV/PDF/JPEG/PNG/WebP signature (`INVALID_FORMAT`, 422). Plain-text files skip this check.

Which uploads a job accepts:

* `POST /v1/audio`: `.mp3` and `.wav` only.
* `POST /v1/image`: `.jpg`, `.jpeg`, `.png`, `.webp` only.
* `POST /v1/text` (`fileId`): `.pdf` and `.txt` only.

## Errors

| Code                   | HTTP | When                                                       |
| ---------------------- | ---- | ---------------------------------------------------------- |
| `INVALID_FORMAT`       | 422  | Extension not allowed, or content doesn't match its format |
| `FILE_TOO_LARGE`       | 413  | Declared or actual size exceeds your plan limit            |
| `UPLOAD_NOT_FOUND`     | 404  | Unknown id, or upload belongs to another account           |
| `UPLOAD_EXPIRED`       | 410  | More than 24 hours since creation                          |
| `UPLOAD_NOT_COMPLETED` | 409  | Job referenced an upload whose file was never `PUT`        |
