---
title: Serve private images
description: Restrict access to Cloudflare Images by generating signed URL tokens with expiration times.
image: https://edgetunnel-b2h.pages.dev/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://edgetunnel-b2h.pages.dev/images/llms.txt  
> Use this file to discover all available pages before exploring further. 

[Skip to content](#%5Ftop) 

# Serve private images

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** ](https://dash.cloudflare.com/?to=/:account/images/hosted)
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](https://edgetunnel-b2h.pages.dev/workers/wrangler/commands/general/#secret) and access it via the `env` parameter. For more information, refer to [Secrets](https://edgetunnel-b2h.pages.dev/workers/configuration/secrets/).

* [  JavaScript ](#tab-panel-9713)
* [  TypeScript ](#tab-panel-9714)

**JavaScript**

```js
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);
  },
};
```

**TypeScript**

```ts
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>;
```

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://edgetunnel-b2h.pages.dev/images/optimization/hosted-images/serve-private-images/#page","headline":"Serve private images · Cloudflare Images docs","description":"Restrict access to Cloudflare Images by generating signed URL tokens with expiration times.","url":"https://edgetunnel-b2h.pages.dev/images/optimization/hosted-images/serve-private-images/","inLanguage":"en","image":"https://edgetunnel-b2h.pages.dev/dev-products-preview.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://edgetunnel-b2h.pages.dev/#website","name":"Cloudflare Docs","url":"https://edgetunnel-b2h.pages.dev/"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/images/","name":"Cloudflare Images"}},{"@type":"ListItem","position":3,"item":{"@id":"/images/optimization/","name":"Optimization"}},{"@type":"ListItem","position":4,"item":{"@id":"/images/optimization/hosted-images/","name":"Hosted images"}},{"@type":"ListItem","position":5,"item":{"@id":"/images/optimization/hosted-images/serve-private-images/","name":"Serve private images"}}]}
```
