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 namefile(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
- 201
- 400
- 401
- 402
- 403
- 429
- 500
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.
Validation error — see error.code and error.details
Missing/invalid/revoked/expired API key (codes invalid_api_key, api_key_revoked, api_key_expired, api_key_required, auth_failed). Auth-layer 401s omit error.retryable and meta.request_id.
No active subscription (code subscription_required — the error object also carries subscription_status), or insufficient AI credits (insufficient_credits) / paid plan required (upgrade_required).
API key missing the required scope (insufficient_scope), storage quota exceeded (storage_quota_exceeded), or account limit reached (account_limit_reached)
Rate limit exceeded (rate_limit_exceeded, or plan budgets post_limit_reached / daily_limit_reached on schedulePost). All 429s carry Retry-After. The API-key limiter additionally sets the X-RateLimit-* headers and error.details {limit, remaining, reset_at}.
Response Headers
Seconds to wait before retrying
Requests allowed in the window (API-key limiter only)
Requests remaining (API-key limiter only)
Unix seconds when the window resets (API-key limiter only)
Internal error (internal_error and endpoint-specific 5xx codes). Retryable.