Skip to main content
Signed URLs add a time limit — and optionally a country restriction or an IP lock — to file and video links, so they can’t be shared or reused forever. They work for both Storage and Stream, as long as the matching tokenSecurityKey is set.
payload.config.ts
Signed URLs secure downloads (how files are served). For securing uploads — minting short-lived URLs so the browser can send files straight to Bunny — see Client uploads.

Options

Getting your token security keys

Bunny dashboard → Delivery → CDN → your Pull Zone → Security → Token Authentication → enable it and copy the key.

Requirements

  • If signedUrls is enabled anywhere storage is configured, storage.tokenSecurityKey is required.
  • If signedUrls is enabled anywhere stream is configured, stream.tokenSecurityKey is required.
Without the matching key, the plugin’s options validator throws a startup error.
The tokenSecurityKey is per zone / per library. When a collection points at its own storage zone or stream library, that zone/library must carry its own tokenSecurityKey for signed URLs to work on it — the global key isn’t inherited.
A Bunny token covers the query string, so any parameter added after signing invalidates it. Payload’s upload.cacheTags appends the document’s updatedAt to admin thumbnails, which would break those links — the plugin therefore sets cacheTags: false on collections that hand signed CDN URLs straight to the browser (disablePayloadAccessControl: true). Filenames are already unique per upload, so cache busting isn’t needed there.

Proxy vs. redirect (staticHandler)

When a collection uses Payload’s access control (the default), a file request goes through Payload first. staticHandler controls what happens next:
Redirect is recommendedProxying downloads the file from Bunny to your server and back out to the user on every request — a redirect sends the user straight to Bunny’s CDN, so your server does none of that work. It also lets Stream videos play as HLS instead of falling back to MP4 — see Access control and mp4Fallback.

IP locking (userIp)

userIp binds each signed link to a single client IPv4 address — a leaked URL stops working from any other IP. You provide the callback because reliably extracting the real client IP depends on your host and proxy chain:
How it behaves:
  • The IP goes into the token hash only — it is never added to the URL.
  • When the collection sets generateFileURL, the plugin keeps that URL and does not re-sign it with the IP.
  • Return a falsy value when no IP can be determined; the link is then signed without an IP lock rather than failing the request.
  • The callback runs only for URLs the client itself will open — document url fields, admin thumbnails, and staticHandler redirects. URLs your server fetches on the client’s behalf (proxied downloads) and URLs generated outside a request (cache purging) are never IP-locked, since Bunny would see your server’s IP there, not the client’s.
Bunny-side requirements
  • Enforcement requires Token IP Validation to be enabled on Bunny: Pull Zone → Security → Token Authentication for Storage, or the equivalent setting on your Stream library. Without it, Bunny ignores the IP in the hash — but a token signed with an IP won’t validate, so only enable userIp together with the Bunny-side setting.
  • Only IPv4 is supported; enabling Token IP Validation disables IPv6 routing on the Pull Zone. Values that aren’t a plain IPv4 address are ignored (the link is signed without a lock) and a warning is logged.
Clients behind carrier-grade NAT or switching networks (mobile) can legitimately change IPs mid-session, which breaks IP-locked links. Prefer short expiresIn values over IP locking for those audiences, or combine both with a tolerant userIp callback.

Expiry (expiresIn)

A number is a sliding window: the link expires that many seconds after it is signed. A function decides per URL. It gets { collection, filename, req?, defaultValue } and returns:
  • a number: seconds from now,
  • a Date: a fixed time, for example the end of a live event or a rental period,
  • undefined: use defaultValue.
defaultValue is the value the URL would get without your function: the global expiresIn result on a collection, or 7200 in the global options.
A number must be more than 0 and at most 10 years, because a UNIX timestamp passed as a number is almost always a mistake. The plugin checks a fixed number at startup and a returned number when it signs the URL. Return a Date for a fixed time.

Example: sign only videos, restrict by country

Collection overrides

See Collection overrides for the full list of options you can tune per collection.