Serve private images
Last updated Apr 21, 2026 You can serve private images by using signed URL tokens. When an image requires a signed URL, the image cannot be accessed without a token unless it is being requested for a variant set to always allow public access.
- In the Cloudflare dashboard, go to the Hosted Images page. Go to Hosted images ↗
- Select Keys.
- Copy your key and use it to generate an expiring tokenized URL.
Note
Private images do not currently support custom paths.
Generate signed URLs from your backend
Signed URLs are generated server-side to protect your signing key. The example below uses a Cloudflare Worker, but the same signing logic can be implemented in any backend environment (Node.js, Python, PHP, Go, etc.).
The Worker accepts a regular Images URL and returns a signed URL that expires after one day. Adjust the EXPIRATION value to set a different expiry period.
Note
Never hardcode your signing key in source code. Store it as a secret using npx wrangler secret put and access it via the env parameter. For more information, refer to Secrets.
const EXPIRATION = 60 * 60 * 24; // 1 day
const bufferToHex = (buffer) =>
[...new Uint8Array(buffer)]
.map((x) => x.toString(16).padStart(2, "0"))
.join("");
async function generateSignedUrl(url, signingKey) {
// `url` is a full imagedelivery.net URL
// e.g. https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(signingKey);
const key = await crypto.subtle.importKey(
"raw",
secretKeyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
// Attach the expiration value to the URL
const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set("exp", expiry);
// `url` now looks like
// https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275
const stringToSign = url.pathname + "?" + url.searchParams.toString();
// e.g. /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275
// Generate the HMAC signature
const mac = await crypto.subtle.sign(
"HMAC",
key,
encoder.encode(stringToSign),
);
const sig = bufferToHex(new Uint8Array(mac).buffer);
// Attach the signature to the URL
url.searchParams.set("sig", sig);
return new Response(url);
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const imageDeliveryURL = new URL(
url.pathname
.slice(1)
.replace("https:/imagedelivery.net", "https://imagedelivery.net"),
);
// IMAGES_SIGNING_KEY is set via `npx wrangler secret put IMAGES_SIGNING_KEY`
return generateSignedUrl(imageDeliveryURL, env.IMAGES_SIGNING_KEY);
},
};
const EXPIRATION = 60 * 60 * 24; // 1 day
const bufferToHex = (buffer: ArrayBuffer) =>
[...new Uint8Array(buffer)]
.map((x) => x.toString(16).padStart(2, "0"))
.join("");
async function generateSignedUrl(
url: URL,
signingKey: string,
): Promise<Response> {
// `url` is a full imagedelivery.net URL
// e.g. https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(signingKey);
const key = await crypto.subtle.importKey(
"raw",
secretKeyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
// Attach the expiration value to the URL
const expiry = Math.floor(Date.now() / 1000) + EXPIRATION;
url.searchParams.set("exp", expiry);
// `url` now looks like
// https://imagedelivery.net/cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275
const stringToSign = url.pathname + "?" + url.searchParams.toString();
// e.g. /cheeW4oKsx5ljh8e8BoL2A/bc27a117-9509-446b-8c69-c81bfeac0a01/mobile?exp=1631289275
// Generate the HMAC signature
const mac = await crypto.subtle.sign(
"HMAC",
key,
encoder.encode(stringToSign),
);
const sig = bufferToHex(new Uint8Array(mac).buffer);
// Attach the signature to the URL
url.searchParams.set("sig", sig);
return new Response(url);
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
const imageDeliveryURL = new URL(
url.pathname
.slice(1)
.replace("https:/imagedelivery.net", "https://imagedelivery.net"),
);
// IMAGES_SIGNING_KEY is set via `npx wrangler secret put IMAGES_SIGNING_KEY`
return generateSignedUrl(imageDeliveryURL, env.IMAGES_SIGNING_KEY);
},
} satisfies ExportedHandler<Env>;
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/#page","headline":"Serve private images","description":"Restrict access to Cloudflare Images by generating signed URL tokens with expiration times.","url":"https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/","inLanguage":"en","image":"https://developers.cloudflare.com/images/optimization/hosted-images/serve-private-images/og.png?v=7299ee84ae1a2d1a","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Construct delivery URLs to serve images uploaded to Cloudflare Images using your account hash, image ID, and variant name. title: Serve uploaded images image: https://developers.cloudflare.com/images/optimization/hosted-images/serve-uploaded-images/og.png?v=8af0924179a84c07
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/images/llms.txt
Use this file to discover all available pages before exploring further.
Docs