Skip to main content

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 caseEndpoint
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 videoPOST /media/upload → PUT presigned URL → POST /media/{id}/complete
You need precise control over width / height / duration metadataPOST /media/upload
Images only — for now

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-urlContent-Type: application/json

FieldTypeRequiredDescription
urlstringYesPublic HTTPS URL pointing to the image. Must be reachable from the public internet — URLs that resolve to private/loopback/link-local addresses are rejected.
filenamestringNoOptional 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 readymedia_status is ready, not uploading. You can paste it straight into a POST /v1/posts request without polling.

Errors

StatuscodeWhen
400validation_errorMissing url field, or url is not a string.
400invalid_urlURL is malformed, uses a non-http(s) protocol, points to localhost, or resolves to a private/loopback IP.
400fetch_failedThe source URL returned a non-2xx response or no body.
400validation_errorSource returned an unsupported content-type for an image.
413file_too_largeSource file exceeded 25 MB.
415unsupported_media_typeSource URL is a video. Use the 3-step flow.
429rate_limit_exceededHourly (200/hr) or daily (1000/day) media upload limit hit.
502service_unavailableCloudflare 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-Type header 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 a 502.