Media & Files
Files live in an R2 bucket, served through signed URLs. Images get on-the-fly transforms at the edge, cached for a year.
Serving
Objects are served at /__r2/{key} only with a valid HMAC-SHA256 signature and unexpired timestamp — direct unsigned access returns 403. Templates access files through the file data source, which returns metadata plus a signed URL (default TTL: 1 hour, configurable via expires).
Uploads
Uploads use a two-step signed-URL flow, so file bytes pass through the API exactly once:
- Mint an upload URL (requires your API JWT):
curl -X POST https://<worker>/api/files/upload-url \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"key": "blog/cover.jpg", "contentType": "image/jpeg", "expiresIn": 600}'
- Upload the raw bytes directly with HTTP PUT to the returned URL before it expires:
curl -X PUT "<signed-url>" \
-H "Content-Type: image/jpeg" \
--data-binary @cover.jpg
Over MCP, the create_upload_url tool does step 1 — agents upload files themselves in one round trip.
Image optimization
Every image URL — static assets and R2 files alike — accepts transformation parameters served by Cloudflare Images: resize and crop (w, h, fit, gravity), format negotiation (f=auto serves AVIF/WebP per browser), compression (q), device pixel ratio (dpr), and more. Transformed variants are generated once and cached at the edge for a year.
<!-- R2 signed URLs: append transforms with & -->
<img src="{{ data.cover.url }}&w=800&h=600&fit=cover&f=auto&q=80" />
For the full parameter reference, format negotiation, srcset patterns, lazy-loading strategy, and quality guidance, see Image Optimization.