# Upload media: single file and parts

> How to upload a video, cover or photo with the Praeko API: a single request (up to 50 MiB), multipart upload with 5 to 95 MiB parts, accepted types, limits and errors, and how to publish a single photo on Instagram.

Source: https://praeko.app/docs/en/guides/media

Every video, cover and photo goes through `/v1/media` before it can be used in a post. There are two paths, depending on size.

|  | Single file | In parts |
|---|---|---|
| When | Up to 50 MiB | Any size (video up to 5 GiB) |
| How to create | `POST /v1/media` with `simple: true` | `POST /v1/media` without `simple` |
| How to upload | `PUT /v1/media/:id/content` with the bytes | `PUT /v1/media/:id/parts/:part` for each part |
| How to finish | The `PUT` itself completes it | `POST /v1/media/:id/complete` |

## Accepted types and sizes

| Type (`type`) | Accepted `mime` | Maximum size |
|---|---|---|
| `video` | `video/mp4`, `video/quicktime` | 5 GiB |
| `cover` | `image/png`, `image/jpeg` | 20 MiB |
| `image` | `image/jpeg` (JPEG only) | 20 MiB |

`image` is the photo of an image post. The server checks that the file really is a JPEG (`422 invalid_image` if not: on a single upload, before storing; on a multipart upload, on complete) and reads the width and height from the header (`width` and `height` on the media item). It does **not convert**: export the photo as sRGB JPEG (Praeko Studio does that).

## A single photo on Instagram

Upload the photo with `type: image` and create the post with `kind: image` and **one** image in `media.images` (several would be a carousel, which does not exist yet: `422 carousel_not_supported`). Only Instagram takes a photo for now, through Buffer or directly through Meta; YouTube and TikTok answer `422 kind_not_supported_by_network`.

**json**

```json
{
  "kind": "image",
  "title": "Tip: small PRs",
  "media": {
    "images": [
      {
        "media": "01KA3V9...",
        "alt": "Slide with the title of the tip"
      }
    ]
  },
  "texts": {
    "instagram": {
      "text": "One PR, one idea. #devbrasil"
    }
  },
  "items": [
    {
      "network": "instagram",
      "via": "meta",
      "queue": true
    }
  ]
}
```

| Instagram rule | Value | If not met |
|---|---|---|
| Format | JPEG | `422 invalid_image` |
| Size | 8 MB | `422 image_too_large` |
| Aspect ratio | from 4:5 to 1.91:1 (use 1080x1350) | `422 image_aspect_ratio` |
| Width | 320 to 1440 px (Meta upscales or downsizes the rest) | warning `image_small` or `image_will_be_resized` |
| Caption | up to 2200 characters and 30 hashtags | `422 invalid_field` |
| Alt text (`alt`) | up to 1000 characters | through Buffer it is ignored: warning `alt_ignored` |

Warnings come back in `items[].warnings` (each with a stable `code` and `message`) and do not stop the creation. Through Meta the `alt` goes in the photo container; through Buffer, which has no documented alt text, it is ignored and the item warns. The post comes back with `kind` and `media.images`; the image of an image post cannot be replaced by `PATCH` (cancel and create again).

## Single file

**curl**

```bash
curl -X POST https://praeko.app/v1/media \
  -H "Authorization: Bearer $PRAEKO_KEY" -H "X-Praeko-Contract: en" \
  -H "Content-Type: application/json" \
  -d '{ "type": "video", "name": "v.mp4", "mime": "video/mp4", "size": 8123456, "simple": true }'
curl -X PUT https://praeko.app/v1/media/$MEDIA_ID/content \
  -H "Authorization: Bearer $PRAEKO_KEY" -H "Content-Type: video/mp4" \
  --data-binary @v.mp4
```

With `simple: true` and a `size` above 50 MiB, creation is rejected (`422 file_too_large`): use parts. The `PUT` body must be exactly the agreed `size` (`422 size_mismatch`).

## Multipart upload

The `POST /v1/media` response (without `simple`) has `minPartSize` (5 MiB), `maxPartSize` (95 MiB) and `suggestedPartSize` (16 MiB). Number the parts from 1 to 10000, send each one with `PUT /v1/media/:id/parts/:part` as raw bytes, and finish with `POST /v1/media/:id/complete`.

- All parts have the same size; only the last one can be smaller (`422 parts_unequal`). Every part except the last must be at least 5 MiB (`422 part_too_small`); none can exceed 95 MiB (`413 part_too_large`).
- Sending the same part again is safe: it is overwritten.
- If a part is missing from the sequence, `complete` returns `422 parts_missing`; if the total does not match the agreed `size`, `422 size_mismatch`.
- An upload that is not finished within 24 hours is discarded (the media leaves the library).

**curl**

```bash
# 16 MiB per part; the last one takes the rest
split -b 16m v.mp4 part_
n=1; for f in part_*; do
  curl -X PUT "https://praeko.app/v1/media/$MEDIA_ID/parts/$n" \
    -H "Authorization: Bearer $PRAEKO_KEY" -H "Content-Type: video/mp4" \
    --data-binary @"$f"
  n=$((n+1))
done
curl -X POST https://praeko.app/v1/media/$MEDIA_ID/complete \
  -H "Authorization: Bearer $PRAEKO_KEY" -H "X-Praeko-Contract: en"
```

## State and use

Media starts as `uploading` and becomes `ready` when the upload finishes. `ready` videos carry `durationMs`, `width` and `height` when the file header provides them (older videos get them on first read), and `GET /v1/media/:id` also returns `publicUrl`. Only use `ready` media in a post (otherwise `409 media_not_ready`). `DELETE /v1/media/:id` refuses media used by a pending post (`409 media_in_use`).

`durationMs` is the duration in milliseconds read from the file. Do not confuse it with `duration` in `GET /v1/youtube/videos`, which is the duration YouTube reports, in ISO 8601 (for example, `PT58S`).

> **With the SDK or an agent** `uploadFile` from `@praeko/sdk/node` sends a file from disk and picks the path for you (one `PUT` up to 32 MiB; above that, 16 MiB parts): [SDK](https://praeko.app/docs/en/sdk). An agent with a shell uses the `create_media_upload` and `complete_media_upload` tools of the [MCP server](https://praeko.app/docs/en/mcp).
