Skip to main content

Upload media (direct or presigned)

POST 

/media/upload

TWO modes, selected by the request Content-Type.

DIRECT mode (images only, recommended for API clients). Send multipart/form-data with the file in a field named file (optional filename field to override the name). The upload completes in this single request and the response returns media_status: "ready" with upload_required: false, no upload_url, no /complete call. Max 25 MB. Videos sent this way get 415 with instructions.

PRESIGNED mode (all types). Send file METADATA as JSON. Returns an upload_url plus upload_method describing how to send the bytes:

  • images → POST multipart/form-data, field name file (Cloudflare Images direct upload)
  • videos (MP4 only) and PDFs → PUT with the declared Content-Type (R2 presigned URL)

Then you MUST call POST /media/{media_id}/complete, or the media stays uploading forever otherwise. expires_at is authoritative (image URLs last 6 hours, video/PDF URLs 1 hour). Size caps: 20 MB images/PDFs, 500 MB video. Upload quota is only consumed at /complete, so retries here are free.

Presigned image caveat: the image upload host runs an edge bot filter outside our control. Send any custom User-Agent header; Python stdlib's default (Python-urllib/x) is rejected with HTTP 403 error 1010. python-requests, curl, node, Go and empty User-Agents all pass. DIRECT mode has no such restriction from any client.

Error codes: validation_error, unsupported_media_type, file_too_large (413), length_required (411, direct mode), storage_quota_exceeded (403), rate_limit_exceeded, service_unavailable.

Request

Responses

Presigned mode: an upload ticket (upload_url + upload_method, then call /complete). Direct mode: the finished media, media_status: "ready", upload_required: false, url set; attach media_ids to a post immediately.