Upload media: single file and parts
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.
{
"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 -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.mp4With 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,
completereturns422 parts_missing; if the total does not match the agreedsize,422 size_mismatch. - An upload that is not finished within 24 hours is discarded (the media leaves the library).
# 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).
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. An agent with a shell uses the create_media_upload and complete_media_upload tools of the MCP server.