Upload media from URL
POST /v1/media/upload-from-url
A one-call alternative to the standard 3-step upload flow. Pass a public URL, get back a media_id ready to attach to a post — no presigning, no separate complete call.
Designed for AI agents that already have URLs (web search results, OG images, hosted screenshots) and for any caller who doesn't want to manage the multi-step upload state machine.
When to use this vs POST /v1/media/upload
| Use case | Endpoint |
|---|---|
| You have a public image URL (most common for AI agents) | /media/upload-from-url ✅ |
| You have an image as bytes on disk (uploading from a local file) | POST /media/upload → PUT presigned URL → POST /media/{id}/complete |
| You need to upload a video | POST /media/upload → PUT presigned URL → POST /media/{id}/complete |
You need precise control over width / height / duration metadata | POST /media/upload |
This endpoint accepts images only (JPEG, PNG, GIF, WebP, HEIC, HEIF) up to 25 MB. Video URLs return 415 unsupported_media_type with a hint to use the 3-step flow. Larger files also need the 3-step flow.
Request
POST /v1/media/upload-from-url — Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Public HTTPS URL pointing to the image. Must be reachable from the public internet — URLs that resolve to private/loopback/link-local addresses are rejected. |
filename | string | No | Optional filename to record. If omitted, derived from the URL path. |
Example
curl -X POST https://app.posteverywhere.ai/api/v1/media/upload-from-url \
-H "Authorization: Bearer pe_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hero.webp"
}'
Response
{
"data": {
"media_id": "a1b2c3d4-...",
"media_ids": ["a1b2c3d4-..."],
"media_status": "ready",
"type": "image",
"url": "https://imagedelivery.net/.../img_.../public",
"filename": "hero.webp",
"size": 14914,
"content_type": "image/webp",
"source_url": "https://example.com/hero.webp",
"next_step": "Attach to a post via POST /v1/posts with media_ids: [\"a1b2c3d4-...\"]."
}
}
The returned media_id is immediately ready — media_status is ready, not uploading. You can paste it straight into a POST /v1/posts request without polling.
Errors
| Status | code | When |
|---|---|---|
400 | validation_error | Missing url field, or url is not a string. |
400 | invalid_url | URL is malformed, uses a non-http(s) protocol, points to localhost, or resolves to a private/loopback IP. |
400 | fetch_failed | The source URL returned a non-2xx response or no body. |
400 | validation_error | Source returned an unsupported content-type for an image. |
413 | file_too_large | Source file exceeded 25 MB. |
415 | unsupported_media_type | Source URL is a video. Use the 3-step flow. |
429 | rate_limit_exceeded | Hourly (200/hr) or daily (1000/day) media upload limit hit. |
502 | service_unavailable | Cloudflare Images upload failed downstream. Retry in a moment. |
Rate limits
This endpoint consumes the same per-hour and per-day media upload counters as POST /v1/media/upload + complete:
- 200 uploads per hour per user
- 1,000 uploads per day per user
If you're running large batches (e.g. an agency scheduling 1,000s of posts), pace your calls to stay under the hourly cap or stagger across multiple days.
Security notes
- SSRF protection: URLs that resolve to private IP ranges (10.x, 192.168.x, 172.16-31.x, 127.x, 169.254.x, IPv6 ULA/link-local) are rejected before any fetch happens.
- Size cap: 25 MB hard cap with streamed download — the connection is closed as soon as the threshold is exceeded.
- Fetch timeout: 30 seconds end-to-end.
- Source content-type: we trust the
Content-Typeheader on the response; if a server lies about an image being a JPEG and serves something else, the image will fail validation at Cloudflare Images and we'll return a502.