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.

  1. In the Cloudflare dashboard, go to the Hosted Images page. Go to Hosted images ↗
  2. Select Keys.
  3. 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

Serve private images - 图1Docs

{"@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


Skip to content

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.