Store and transform images with Cloudflare Images API and transformations. Use when: uploading images, implementing direct creator uploads, creating variants, generating signed URLs, optimizing formats (WebP/AVIF), transforming via Workers, or debugging CORS, multipart, or error codes 9401-9413.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdreferences/api-reference.mdreferences/direct-upload-complete-workflow.mdreferences/format-optimization.mdreferences/responsive-images-patterns.mdreferences/signed-urls-guide.mdreferences/top-errors.mdreferences/transformation-options.mdreferences/variants-guide.mdrules/cloudflare-images.mdscripts/check-versions.shtemplates/batch-upload.tstemplates/direct-creator-upload-backend.tstemplates/direct-creator-upload-frontend.htmltemplates/package.jsontemplates/responsive-images-srcset.htmltemplates/signed-urls-generation.tstemplates/transform-via-url.tstemplates/transform-via-workers.tstemplates/upload-api-basic.tsStatus: Production Ready ✅ Last Updated: 2025-11-23 Dependencies: Cloudflare account with Images enabled Latest Versions: Cloudflare Images API v2, @cloudflare/workers-types@4.20251121.0
Recent Updates (2025):
gravity=face with zoom control, GPU-based RetinaFace, 99.4% precision)Two features: Images API (upload/store with variants) and Image Transformations (resize any image via URL or Workers).
1. Enable: Dashboard → Images → Get Account ID + API token (Cloudflare Images: Edit permission)
2. Upload:
curl -X POST https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/images/v1 \
-H 'Authorization: Bearer <API_TOKEN>' \
-H 'Content-Type: multipart/form-data' \
-F 'file=@./image.jpg'
3. Serve: https://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/public
4. Transform (optional): Dashboard → Images → Transformations → Enable for zone
<img src="/cdn-cgi/image/width=800,quality=85/uploads/photo.jpg" />
1. File Upload: POST to /images/v1 with file (multipart/form-data), optional id, requireSignedURLs, metadata
2. Upload via URL: POST with url=https://example.com/image.jpg (supports HTTP basic auth)
3. Direct Creator Upload (one-time URLs, no API key exposure):
Backend: POST to /images/v2/direct_upload → returns uploadURL
Frontend: POST file to uploadURL with FormData
CRITICAL CORS FIX:
multipart/form-data (let browser set header)file (NOT image)/direct_upload from backend onlyContent-Type: application/json/direct_upload from browserURL: /cdn-cgi/image/<OPTIONS>/<SOURCE>
width=800,height=600,fit=coverquality=85 (1-100)format=auto (WebP/AVIF auto-detection)gravity=auto (smart crop), gravity=face (AI face detection, Aug 2025 GA), gravity=center, zoom=0.5 (0-1 range, face crop tightness)blur=10,sharpen=3,brightness=1.2scale-down, contain, cover, crop, padWorkers: Use cf.image object in fetch
fetch(imageURL, {
cf: {
image: { width: 800, quality: 85, format: 'auto', gravity: 'face', zoom: 0.8 }
}
});
Named Variants (up to 100): Predefined transformations (e.g., avatar, thumbnail)
/images/v1/variants with id, optionsimagedelivery.net/<HASH>/<ID>/avatarFlexible Variants: Dynamic params in URL (w=400,sharpen=3)
/images/v1/config with {"flexible_variants": true}Generate HMAC-SHA256 tokens for private images (URL format: ?exp=<TIMESTAMP>&sig=<HMAC>).
Algorithm: HMAC-SHA256(signingKey, imageId + variant + expiry) → hex signature
See: templates/signed-urls-generation.ts for Workers implementation
✅ Use multipart/form-data for Direct Creator Upload
✅ Name the file field file (not image or other names)
✅ Call /direct_upload API from backend only (NOT browser)
✅ Use HTTPS URLs for transformations (HTTP not supported)
✅ URL-encode special characters in image paths
✅ Enable transformations on zone before using /cdn-cgi/image/
✅ Use named variants for private images (signed URLs)
✅ Check Cf-Resized header for transformation errors
✅ Set format=auto for automatic WebP/AVIF conversion
✅ Use fit=scale-down to prevent unwanted enlargement
❌ Use application/json Content-Type for file uploads
❌ Call /direct_upload from browser (CORS will fail)
❌ Use flexible variants with requireSignedURLs=true
❌ Resize SVG files (they're inherently scalable)
❌ Use HTTP URLs for transformations (HTTPS only)
❌ Put spaces or unescaped Unicode in URLs
❌ Transform the same image multiple times in Workers (causes 9403 loop)
❌ Exceed 100 megapixels image size
❌ Use /cdn-cgi/image/ endpoint in Workers (use cf.image instead)
❌ Forget to enable transformations on zone before use
This skill prevents 13+ documented issues.
Error: Access to XMLHttpRequest blocked by CORS policy: Request header field content-type is not allowed
Source: Cloudflare Community #345739, #368114
Why It Happens: Server CORS settings only allow multipart/form-data for Content-Type header
Prevention:
// ✅ CORRECT
const formData = new FormData();
formData.append('file', fileInput.files[0]);
await fetch(uploadURL, {
method: 'POST',
body: formData // Browser sets multipart/form-data automatically
});
// ❌ WRONG
await fetch(uploadURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, // CORS error
body: JSON.stringify({ file: base64Image })
});
Error: Error 5408 after ~15 seconds of upload
Source: Cloudflare Community #571336
Why It Happens: Cloudflare has 30-second request timeout; slow uploads or large files exceed limit
Prevention:
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
if (file.size > MAX_FILE_SIZE) {
alert('File too large. Please select an image under 10MB.');
return;
}
Error: 400 Bad Request with unhelpful error message
Source: Cloudflare Community #487629
Why It Happens: File field must be named file (not image, photo, etc.)
Prevention:
// ✅ CORRECT
formData.append('file', imageFile);
// ❌ WRONG
formData.append('image', imageFile); // 400 error
formData.append('photo', imageFile); // 400 error
Error: Preflight OPTIONS request blocked
Source: Cloudflare Community #306805
Why It Happens: Calling /direct_upload API directly from browser (should be backend-only)
Prevention:
ARCHITECTURE:
Browser → Backend API → POST /direct_upload → Returns uploadURL → Browser uploads to uploadURL
Never expose API token to browser. Generate upload URL on backend, return to frontend.
Error: Cf-Resized: err=9401 - Required cf.image options missing or invalid
Source: Cloudflare Images Docs - Troubleshooting
Why It Happens: Missing required transformation parameters or invalid values
Prevention:
// ✅ CORRECT
fetch(imageURL, {
cf: {
image: {
width: 800,
quality: 85,
format: 'auto'
}
}
});
// ❌ WRONG
fetch(imageURL, {
cf: {
image: {
width: 'large', // Must be number
quality: 150 // Max 100
}
}
});
Error: Cf-Resized: err=9402 - Image too large or connection interrupted
Source: Cloudflare Images Docs - Troubleshooting
Why It Happens: Image exceeds maximum area (100 megapixels) or download fails
Prevention:
Error: Cf-Resized: err=9403 - Worker fetching its own URL or already-resized image
Source: Cloudflare Images Docs - Troubleshooting
Why It Happens: Transformation applied to already-transformed image, or Worker fetches itself
Prevention:
// ✅ CORRECT
if (url.pathname.startsWith('/images/')) {
const originalPath = url.pathname.replace('/images/', '');
const originURL = `https://storage.example.com/${originalPath}`;
return fetch(originURL, { cf: { image: { width: 800 } } });
}
// ❌ WRONG
if (url.pathname.startsWith('/images/')) {
// Fetches worker's own URL, causes loop
return fetch(request, { cf: { image: { width: 800 } } });
}
Error: Cf-Resized: err=9406 or err=9419 - Non-HTTPS URL or URL has spaces/unescaped Unicode
Source: Cloudflare Images Docs - Troubleshooting
Why It Happens: Image URL uses HTTP (not HTTPS) or contains invalid characters
Prevention:
// ✅ CORRECT
const imageURL = "https://example.com/images/photo%20name.jpg";
// ❌ WRONG
const imageURL = "http://example.com/images/photo.jpg"; // HTTP not allowed
const imageURL = "https://example.com/images/photo name.jpg"; // Space not encoded
Always use encodeURIComponent() for URL paths:
const filename = "photo name.jpg";
const imageURL = `https://example.com/images/${encodeURIComponent(filename)}`;
Error: Cf-Resized: err=9412 - Origin returned HTML instead of image
Source: Cloudflare Images Docs - Troubleshooting
Why It Happens: Origin server returns 404 page or error page (HTML) instead of image
Prevention:
// Verify URL before transforming
const originResponse = await fetch(imageURL, { method: 'HEAD' });
const contentType = originResponse.headers.get('content-type');
if (!contentType?.startsWith('image/')) {
return new Response('Not an image', { status: 400 });
}
return fetch(imageURL, { cf: { image: { width: 800 } } });
Error: Cf-Resized: err=9413 - Image exceeds 100 megapixels
Source: Cloudflare Images Docs - Troubleshooting
Why It Happens: Source image dimensions exceed 100 megapixels (e.g., 10000x10000px)
Prevention:
const MAX_MEGAPIXELS = 100;
if (width * height > MAX_MEGAPIXELS * 1_000_000) {
return new Response('Image too large', { status: 413 });
}
Error: Flexible variants don't work with private images
Source: Cloudflare Images Docs - Enable flexible variants
Why It Happens: Flexible variants cannot be used with requireSignedURLs=true
Prevention:
// ✅ CORRECT - Use named variants for private images
await uploadImage({
file: imageFile,
requireSignedURLs: true // Use named variants: /public, /avatar, etc.
});
// ❌ WRONG - Flexible variants don't support signed URLs
// Cannot use: /w=400,sharpen=3 with requireSignedURLs=true
Error: SVG files don't resize via transformations
Source: Cloudflare Images Docs - SVG files
Why It Happens: SVG is inherently scalable (vector format), resizing not applicable
Prevention:
// SVGs can be served but not resized
// Use any variant name as placeholder
// https://imagedelivery.net/<HASH>/<SVG_ID>/public
// SVG will be served at original size regardless of variant settings
Error: GPS data, camera settings removed from uploaded JPEGs
Source: Cloudflare Images Docs - Transform via URL
Why It Happens: Default behavior strips all metadata except copyright
Prevention:
// Preserve metadata
fetch(imageURL, {
cf: {
image: {
width: 800,
metadata: 'keep' // Options: 'none', 'copyright', 'keep'
}
}
});
Options:
none: Strip all metadatacopyright: Keep only copyright tag (default for JPEG)keep: Preserve most EXIF metadata including GPSCopy-paste ready code for common patterns:
Usage:
cp templates/upload-api-basic.ts src/upload.ts
# Edit with your account ID and API token
In-depth documentation Claude can load as needed:
When to load:
check-versions.sh - Verify API endpoints are current
Custom Domains: Serve from your domain via /cdn-cgi/imagedelivery/<HASH>/<ID>/<VARIANT> (requires domain on Cloudflare, proxied). Use Transform Rules for custom paths.
Batch API: High-volume uploads via batch.imagedelivery.net with batch tokens (Dashboard → Images → Batch API)
Webhooks: Notifications for Direct Creator Upload (Dashboard → Notifications → Webhooks). Payload includes imageId, status, metadata.
Symptoms: /cdn-cgi/image/... returns original image or 404
Solutions:
Symptoms: Access-Control-Allow-Origin error in browser console
Solutions:
multipart/form-data encoding (let browser set Content-Type)/direct_upload from browser; call from backendfile (not image)Symptoms: Cf-Resized: err=9403 in response headers
Solutions:
Symptoms: 403 Forbidden when accessing signed URL
Solutions:
requireSignedURLs=trueSymptoms: Upload returns 200 OK but image not in dashboard
Solutions:
draft: true in response (Direct Creator Upload)/images/v1/{id})Last Verified: 2025-11-23 API Version: v2 (direct uploads), v1 (standard uploads) Optional: @cloudflare/workers-types@4.20251121.0