Upload in three steps, straight to storage, so a large video never passes through the API:
1. POST /v1/media — say what you are about to upload.
{ "mime_type": "image/jpeg", "filename": "catalogue.jpg", "bytes": 482113 }
mime_type is one of image/jpeg, image/png, image/webp, image/gif,
video/mp4, video/quicktime. Images up to 8 MB, video up to 512 MB.
201:
{
"media": { "id": "e71b0f3d-2c85-4a9e-b604-93d2f7a1c5e8", "mime_type": "image/jpeg", "bytes": null, "url": null, "status": "pending", "created_at": "…" },
"upload": {
"signed_url": "https://…/storage/v1/object/upload/sign/social-media/e71b0f3d-…/catalogue.jpg?token=…",
"token": "…",
"path": "e71b0f3d-2c85-4a9e-b604-93d2f7a1c5e8/catalogue.jpg"
}
}
2. Upload the file with PUT to upload.signed_url, the raw bytes as the
body and the same Content-Type. No API key on this request — the URL is the
credential, and it expires after two hours.
curl -sS -X PUT "$SIGNED_URL" -H "Content-Type: image/jpeg" --data-binary @catalogue.jpg
3. POST /v1/media/{id}/complete — no body. We check the file that
arrived is the type and size you declared, and the media becomes ready.
200 with the media object; 409 if the upload has not finished;
400 if the file does not match, in which case it is deleted and you start
again.
Only ready media can go on a post. An upload never completed is deleted after
a day, and ready media that no draft or scheduled post uses is deleted after 30
days.
Operations
POST /v1/media
Start a media upload
Scope social:write. Accepts Idempotency-Key.
Reserves a media item and returns a signed URL to upload the file to, then call
POST /media/{id}/complete. The organisation's media storage allowance applies
(402 PLAN_LIMIT). A replay with the same Idempotency-Key answers upload: null: the
upload credential is never kept, so ask for a new slot if you still need one.
Request body (required)
| Field | Type | |
|---|---|---|
mime_type |
string |
(required; one of image/jpeg, image/png, image/webp, image/gif, video/mp4, video/quicktime) |
filename |
string |
(required; min 1; max 255) |
bytes |
integer |
(required; >= 1; <= 536870912) |
{
"mime_type": "image/jpeg",
"filename": "<filename>",
"bytes": 1
}
Answers
| Status | Body | |
|---|---|---|
| 201 | MediaUpload |
Reserved. |
| 400 | ErrorEnvelope |
VALIDATION_ERROR: a body that is not JSON, a field that breaks its rule (named in fields), a bad query value or cursor, or a malformed Idempotency-Key. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 402 | ErrorEnvelope |
PLAN_LIMIT: the organisation's plan does not allow more of this. The message names the allowance. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 413 | ErrorEnvelope |
PAYLOAD_TOO_LARGE: the body is over 64 KB. |
| 422 | ErrorEnvelope |
VALIDATION_ERROR: the request is well formed but cannot be done as asked, such as a network that does not support it. Or UPSTREAM_ERROR: the network refused it. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
POST /v1/media/{id}/complete
Finish a media upload
Scope social:write. Accepts Idempotency-Key. Another workspace's answers 404.
Checks the uploaded file and makes the media item ready for posts. Before the upload finishes it is 409 CONFLICT.
Parameters
| Name | In | Type | |
|---|---|---|---|
id |
path | uuid |
An id this API gave you. Anything that is not one of yours is 404. (required) |
Answers
| Status | Body | |
|---|---|---|
| 200 | Media |
The media item. |
| 401 | ErrorEnvelope |
UNAUTHORIZED: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | ErrorEnvelope |
INSUFFICIENT_SCOPE: the key lacks this operation's scope. Or ORGANISATION_SUSPENDED: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | ErrorEnvelope |
NOT_FOUND: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 409 | ErrorEnvelope |
CONFLICT: the request clashes with the object's current state (the message says how). IDEMPOTENCY_KEY_CONFLICT / IDEMPOTENCY_KEY_IN_FLIGHT: the Idempotency-Key was used with another request, or its first request is still running. |
| 422 | ErrorEnvelope |
VALIDATION_ERROR: the request is well formed but cannot be done as asked, such as a network that does not support it. Or UPSTREAM_ERROR: the network refused it. |
| 429 | ErrorEnvelope |
RATE_LIMITED: too many requests. Wait for Retry-After seconds. |
| 500 | ErrorEnvelope |
INTERNAL_ERROR: ours. Safe to retry; send the same Idempotency-Key on a POST. |
Shapes
The objects these operations send and answer.
Media
| Field | Type | |
|---|---|---|
id |
uuid |
(required) |
mime_type |
string |
(required) |
bytes |
integer | null |
(required) |
url |
string | null |
(required) |
status |
string |
(required; one of pending, ready) |
created_at |
date-time |
(required) |
MediaCreate
| Field | Type | |
|---|---|---|
mime_type |
string |
(required; one of image/jpeg, image/png, image/webp, image/gif, video/mp4, video/quicktime) |
filename |
string |
(required; min 1; max 255) |
bytes |
integer |
(required; >= 1; <= 536870912) |
MediaUpload
| Field | Type | |
|---|---|---|
media |
Media |
(required) |
upload |
object | null |
Where to upload the file, once. Null on an idempotent replay. (required) |