Loading...
Loading...
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.
npx skill4agent add ovachiever/droid-tings cloudflare-imagescurl --request POST \
--url https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/images/v1 \
--header 'Authorization: Bearer <API_TOKEN>' \
--header 'Content-Type: multipart/form-data' \
--form 'file=@./image.jpg'idvariantsmultipart/form-dataapplication/json<img src="https://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/public" />public<img src="/cdn-cgi/image/width=800,quality=85/uploads/photo.jpg" />export default {
async fetch(request: Request): Promise<Response> {
const imageURL = "https://example.com/image.jpg";
return fetch(imageURL, {
cf: {
image: {
width: 800,
quality: 85,
format: "auto" // WebP/AVIF for supporting browsers
}
}
});
}
};imagedelivery.net/cdn-cgi/imagedelivery/...templates/upload-api-basic.tstemplates/direct-creator-upload-backend.tswidth=800,height=600,fit=coverquality=85,format=autoblur=10,sharpen=3gravity=face,zoom=0.5templates/transform-via-url.tstemplates/transform-via-workers.tsthumbnailavatarherow=400,sharpen=3templates/variants-management.tsreferences/variants-guide.mdcurl --request POST \
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: multipart/form-data" \
--form 'file=@./image.jpg' \
--form 'requireSignedURLs=false' \
--form 'metadata={"key":"value"}'fileidrequireSignedURLstruefalsemetadata{
"result": {
"id": "2cdc28f0-017a-49c4-9ed7-87056c83901",
"filename": "image.jpg",
"uploaded": "2022-01-31T16:39:28.458Z",
"requireSignedURLs": false,
"variants": [
"https://imagedelivery.net/Vi7wi5KSItxGFsWRG2Us6Q/2cdc28f0.../public"
]
}
}templates/upload-api-basic.tscurl --request POST \
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \
--header "Authorization: Bearer <API_TOKEN>" \
--form 'url=https://example.com/image.jpg' \
--form 'metadata={"source":"external"}'https://user:password@example.com/image.jpgfileurltemplates/upload-via-url.tsconst response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v2/direct_upload`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
requireSignedURLs: true,
metadata: { userId: '12345' },
expiry: '2025-10-26T18:00:00Z' // Optional: default 30min, max 6hr
})
}
);
const { uploadURL, id } = await response.json();
// Return uploadURL to frontend<form id="upload-form">
<input type="file" id="file-input" accept="image/*" />
<button type="submit">Upload</button>
</form>
<script>
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('file-input');
const formData = new FormData();
formData.append('file', fileInput.files[0]); // MUST be named 'file'
const uploadURL = 'UPLOAD_URL_FROM_BACKEND'; // Get from backend
const response = await fetch(uploadURL, {
method: 'POST',
body: formData // NO Content-Type header, browser sets multipart/form-data
});
if (response.ok) {
console.log('Upload successful!');
}
});
</script>multipart/form-datafileimage/direct_uploadContent-Type: application/jsonimage/jpeg/direct_uploadtemplates/direct-creator-upload-backend.tstemplates/direct-creator-upload-frontend.htmlreferences/direct-upload-complete-workflow.mdhttps://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE><img src="/cdn-cgi/image/width=800,quality=85,format=auto/uploads/photo.jpg" />width=800height=600fit=coverquality=85format=autoformat=webpformat=jpeggravity=autogravity=facetrim=10blur=10sharpen=3brightness=1.2contrast=1.1rotate=90flip=hflip=vscale-downcontaincovercroppadbackground<img src="/cdn-cgi/image/format=auto/image.jpg" />templates/transform-via-url.tsreferences/transformation-options.mdexport default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// Custom URL scheme: /images/thumbnail/photo.jpg
if (url.pathname.startsWith('/images/thumbnail/')) {
const imagePath = url.pathname.replace('/images/thumbnail/', '');
const imageURL = `https://storage.example.com/${imagePath}`;
return fetch(imageURL, {
cf: {
image: {
width: 300,
height: 300,
fit: 'cover',
quality: 85
}
}
});
}
return new Response('Not found', { status: 404 });
}
};const accept = request.headers.get('accept') || '';
let format: 'avif' | 'webp' | 'auto' = 'auto';
if (/image\/avif/.test(accept)) {
format = 'avif';
} else if (/image\/webp/.test(accept)) {
format = 'webp';
}
return fetch(imageURL, {
cf: {
image: {
format,
width: 800,
quality: 85
}
}
});thumbnailavatarlargetemplates/transform-via-workers.tsreferences/transformation-options.mdcurl "https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1/variants" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"id": "avatar",
"options": {
"fit": "cover",
"width": 200,
"height": 200,
"metadata": "none"
},
"neverRequireSignedURLs": false
}'<img src="https://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/avatar" />templates/variants-management.tscurl --request PATCH \
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1/config \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{"flexible_variants": true}'<img src="https://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/w=400,sharpen=3" />requireSignedURLs=truereferences/variants-guide.mdhttps://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/<VARIANT>?exp=<EXPIRY>&sig=<SIGNATURE>async function generateSignedURL(
imageId: string,
variant: string,
expirySeconds: number = 3600
): Promise<string> {
const accountHash = 'YOUR_ACCOUNT_HASH';
const signingKey = 'YOUR_SIGNING_KEY'; // Dashboard → Images → Keys
const expiry = Math.floor(Date.now() / 1000) + expirySeconds;
const stringToSign = `${imageId}${variant}${expiry}`;
const encoder = new TextEncoder();
const keyData = encoder.encode(signingKey);
const messageData = encoder.encode(stringToSign);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
const sig = Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return `https://imagedelivery.net/${accountHash}/${imageId}/${variant}?exp=${expiry}&sig=${sig}`;
}const signedURL = await generateSignedURL('image-id', 'public', 3600);
// Returns URL valid for 1 hourtemplates/signed-urls-generation.tsreferences/signed-urls-guide.md<img
srcset="
https://imagedelivery.net/<HASH>/<ID>/mobile 480w,
https://imagedelivery.net/<HASH>/<ID>/tablet 768w,
https://imagedelivery.net/<HASH>/<ID>/desktop 1920w
"
sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1920px"
src="https://imagedelivery.net/<HASH>/<ID>/desktop"
alt="Responsive image"
/><img
srcset="
https://imagedelivery.net/<HASH>/<ID>/w=480,f=auto 480w,
https://imagedelivery.net/<HASH>/<ID>/w=768,f=auto 768w,
https://imagedelivery.net/<HASH>/<ID>/w=1920,f=auto 1920w
"
sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1920px"
src="https://imagedelivery.net/<HASH>/<ID>/w=1920,f=auto"
alt="Responsive image"
/><picture>
<source
media="(max-width: 767px)"
srcset="https://imagedelivery.net/<HASH>/<ID>/mobile-square"
/>
<source
media="(min-width: 768px)"
srcset="https://imagedelivery.net/<HASH>/<ID>/desktop-wide"
/>
<img src="https://imagedelivery.net/<HASH>/<ID>/desktop-wide" alt="Hero image" />
</picture>templates/responsive-images-srcset.htmlreferences/responsive-images-patterns.mdmultipart/form-datafileimage/direct_upload/cdn-cgi/image/Cf-Resizedformat=autofit=scale-downapplication/json/direct_uploadrequireSignedURLs=true/cdn-cgi/image/cf.imageAccess to XMLHttpRequest blocked by CORS policy: Request header field content-type is not allowedmultipart/form-data// ✅ 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 5408const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
if (file.size > MAX_FILE_SIZE) {
alert('File too large. Please select an image under 10MB.');
return;
}400 Bad Requestfileimagephoto// ✅ CORRECT
formData.append('file', imageFile);
// ❌ WRONG
formData.append('image', imageFile); // 400 error
formData.append('photo', imageFile); // 400 error/direct_uploadARCHITECTURE:
Browser → Backend API → POST /direct_upload → Returns uploadURL → Browser uploads to uploadURLCf-Resized: err=9401// ✅ 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
}
}
});Cf-Resized: err=9402Cf-Resized: err=9403// ✅ 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 } } });
}Cf-Resized: err=9406err=9419// ✅ 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 encodedencodeURIComponent()const filename = "photo name.jpg";
const imageURL = `https://example.com/images/${encodeURIComponent(filename)}`;Cf-Resized: err=9412// 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 } } });Cf-Resized: err=9413const MAX_MEGAPIXELS = 100;
if (width * height > MAX_MEGAPIXELS * 1_000_000) {
return new Response('Image too large', { status: 413 });
}requireSignedURLs=true// ✅ 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// 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// Preserve metadata
fetch(imageURL, {
cf: {
image: {
width: 800,
metadata: 'keep' // Options: 'none', 'copyright', 'keep'
}
}
});nonecopyrightkeepcp templates/upload-api-basic.ts src/upload.ts
# Edit with your account ID and API tokenimagedelivery.nethttps://example.com/cdn-cgi/imagedelivery/<ACCOUNT_HASH>/<IMAGE_ID>/<VARIANT>/images/.../cdn-cgi/imagedelivery/...starts_with(http.request.uri.path, "/images/")/cdn-cgi/imagedelivery/<ACCOUNT_HASH>${substring(http.request.uri.path, 7)}/images/{id}/{variant}/cdn-cgi/imagedelivery/{hash}/{id}/{variant}batch.imagedelivery.netapi.cloudflare.com# Create batch token in dashboard: Images → Batch API
curl "https://batch.imagedelivery.net/images/v1" \
--header "Authorization: Bearer <BATCH_TOKEN>" \
--form 'file=@./image.jpg'templates/batch-upload.ts{
"imageId": "2cdc28f0-017a-49c4-9ed7-87056c83901",
"status": "uploaded",
"metadata": {"userId": "12345"}
}/cdn-cgi/image/...Access-Control-Allow-Originmultipart/form-data/direct_uploadfileimageCf-Resized: err=9403requireSignedURLs=truedraft: true/images/v1/{id}@cloudflare/workers-types@latestreferences/top-errors.md