Skip to main content

Signed URLs

Signed URLs let you protect content behind expiring, tamper-proof links. The edge only serves a protected path when the request carries a valid HMAC token that you generated with the property's secret — anything else gets a 403. Typical uses: paid downloads, private media, and preventing hotlinking of expensive assets.

Signed URLs are available on Starter plans and above — see Plans & pricing.

Enabling URL signing

  1. Open your property and go to the General tab.
  2. In the URL Signing card, toggle it on. A signing secret is generated for you — a random 32-byte hex string. Copy it; your backend needs it to sign links.
  3. Optionally add protected paths — a list of path prefixes such as /downloads or /private. Requests under any listed prefix require a valid token.
  4. Leave the list empty to protect the entire property — every request will require a token.
  5. Click Save on the card.
The secret is server-generated

You cannot choose the secret — it is always generated server-side to guarantee its strength. Regenerating it invalidates all previously issued links immediately, so re-sign links after a rotation.

The configuration is stored in the property's general-settings JSON under url_signing:

{
"url_signing": {
"enabled": true,
"secret": "0f9c2d…64 hex chars…a1b7",
"paths": ["/downloads", "/private"]
}
}
FieldTypeMeaning
enabledbooleanWhether signing is enforced.
secretstringServer-generated signing secret (32 random bytes, hex).
pathsstring[]Protected path prefixes. Empty or absent = the whole property is protected.

How the token works

A signed link adds two query parameters to the URL (plus an optional third for folder signatures):

ParameterValue
pdm_expiresExpiry time as Unix epoch seconds (UTC).
pdm_tokenLowercase hex of HMAC-SHA256(secret, path + ":" + expires).
pdm_pathOptional. Folder prefix the token was signed for — see Sign an entire folder.

The signed message is the URI path only — no query string, no scheme, no host. For example, for /downloads/report.pdf expiring at 1767225600, the token is the HMAC-SHA256 of the string /downloads/report.pdf:1767225600.

A request to a protected path is rejected with 403 when the token is missing, does not match, or pdm_expires is in the past. The edge compares tokens in constant time, so the check is not vulnerable to timing attacks.

Sign the path exactly as requested

The edge signs the request's URI path verbatim. Generate the token for the exact path the client will request — same case, same encoding — or the HMACs won't match.

Sign an entire folder

An exact-file token is only valid for one path. To cover a whole directory tree with a single signature, add the optional pdm_path query parameter:

  • pdm_path is a folder prefix — it must start and end with /, for example /videos/curso-1/.
  • When pdm_path is present, the token is the lowercase hex of HMAC-SHA256(secret, pdm_path + ":" + expires) — you sign the prefix, not the request path.
  • The edge then requires the request's URI path to start with that prefix. Any file under the folder (at any depth) is accepted with the same token; anything outside it gets a 403.
  • Every child URL carries the same three query parameters: pdm_path, pdm_expires, pdm_token. Sign once, then append the identical query string to each file's URL.

For example, one signature of /videos/curso-1/ covers all of these:

https://www.example.com/videos/curso-1/master.m3u8?pdm_path=/videos/curso-1/&pdm_expires=1767225600&pdm_token=9c41…
https://www.example.com/videos/curso-1/720p/segment-000.ts?pdm_path=/videos/curso-1/&pdm_expires=1767225600&pdm_token=9c41…
https://www.example.com/videos/curso-1/720p/segment-001.ts?pdm_path=/videos/curso-1/&pdm_expires=1767225600&pdm_token=9c41…
Made for HLS

Folder signatures are the natural fit for video streaming: an HLS session requests a playlist plus hundreds of segments. Sign the stream's folder once and append the same pdm_path + pdm_expires + pdm_token to the playlist URL and to every segment URL — no per-segment signing, and players that resolve relative segment URLs against the playlist URL keep working.

Prefix, not substring

The match is a clean path-prefix check: a token for /videos/curso-1/ is valid for /videos/curso-1/intro.mp4 but never for /videos/curso-10/… or for paths outside the folder. A pdm_path that does not start with /, or that contains .., is rejected with 403.

Generating signed URLs

The examples below sign an exact file (/downloads/report.pdf) and a whole folder (/videos/curso-1/) with a one-hour expiry. Replace the secret with your property's signing secret and the hostname with any hostname of the property.

bash (openssl)

SECRET="your-signing-secret"
EXPIRES=$(($(date +%s) + 3600)) # now + 1 hour

# --- Exact file: sign the request path itself ---
SIGN_PATH="/downloads/report.pdf"
TOKEN=$(printf '%s' "${SIGN_PATH}:${EXPIRES}" \
| openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)

echo "https://www.example.com${SIGN_PATH}?pdm_expires=${EXPIRES}&pdm_token=${TOKEN}"

# --- Whole folder: sign the prefix once, reuse it for every file under it ---
FOLDER="/videos/curso-1/" # must start and end with '/'
TOKEN=$(printf '%s' "${FOLDER}:${EXPIRES}" \
| openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)
QS="pdm_path=${FOLDER}&pdm_expires=${EXPIRES}&pdm_token=${TOKEN}"

echo "https://www.example.com${FOLDER}master.m3u8?${QS}"
echo "https://www.example.com${FOLDER}720p/segment-000.ts?${QS}"

PHP

A complete, copy-paste helper. Pass a full URL (or a bare absolute path); set $isFolder = true to sign a folder prefix instead of an exact file. URLs that already carry a query string are handled (& vs ?).

<?php

declare(strict_types=1);

/**
* Build a signed Paradarum URL.
*
* @param string $secret The property's signing secret (from the URL Signing card).
* @param string $path Full URL (https://host/path…) or absolute path (/videos/intro.mp4).
* With $isFolder = true it must be a folder ending in '/'.
* @param int $ttlSeconds How long the link stays valid, in seconds from now.
* @param bool $isFolder true = folder signature (pdm_path); false = exact-file signature.
*
* @return string The input URL/path with pdm_expires, pdm_token (and pdm_path) appended.
*/
function paradarum_sign_url(string $secret, string $path, int $ttlSeconds, bool $isFolder = false): string
{
// Extract the URI path — the only part of the URL that gets signed.
$uriPath = parse_url($path, PHP_URL_PATH);
if (!is_string($uriPath) || $uriPath === '' || $uriPath[0] !== '/') {
throw new InvalidArgumentException('Could not extract an absolute URI path from: ' . $path);
}
if ($isFolder && substr($uriPath, -1) !== '/') {
throw new InvalidArgumentException('Folder signatures require a path ending in "/": ' . $uriPath);
}

$expires = time() + $ttlSeconds;

$params = [];
if ($isFolder) {
// Folder mode: the token signs the pdm_path prefix, not the request path.
$params['pdm_path'] = $uriPath;
}
$params['pdm_expires'] = $expires;
$params['pdm_token'] = hash_hmac('sha256', $uriPath . ':' . $expires, $secret); // lowercase hex

// Append with '&' when the URL already has a query string, with '?' otherwise.
$existingQuery = parse_url($path, PHP_URL_QUERY);
$separator = (is_string($existingQuery) && $existingQuery !== '') ? '&' : '?';

return $path . $separator . http_build_query($params);
}

// --- Example 1: exact file — token valid ONLY for /videos/intro.mp4 ---
// echo paradarum_sign_url('your-signing-secret', 'https://midominio.com/videos/intro.mp4', 3600);
// → https://midominio.com/videos/intro.mp4?pdm_expires=1767225600&pdm_token=5f8a…

// --- Example 2: whole folder — one signature for EVERYTHING under /videos/curso-1/ ---
// echo paradarum_sign_url('your-signing-secret', 'https://midominio.com/videos/curso-1/', 3600, true);
// → https://midominio.com/videos/curso-1/?pdm_path=%2Fvideos%2Fcurso-1%2F&pdm_expires=1767225600&pdm_token=9c41…
// Append that same query string (pdm_path + pdm_expires + pdm_token) to every
// file under the folder, e.g. …/curso-1/master.m3u8 and each …/curso-1/*.ts segment.

Node.js

const crypto = require('node:crypto');

const secret = 'your-signing-secret';
const expires = Math.floor(Date.now() / 1000) + 3600; // now + 1 hour

const hmac = (message) =>
crypto.createHmac('sha256', secret).update(message).digest('hex'); // lowercase hex

// --- Exact file: sign the request path itself ---
const path = '/downloads/report.pdf';
const token = hmac(`${path}:${expires}`);
console.log(`https://www.example.com${path}?pdm_expires=${expires}&pdm_token=${token}`);

// --- Whole folder: sign the prefix once, reuse it for every file under it ---
const folder = '/videos/curso-1/'; // must start and end with '/'
const folderToken = hmac(`${folder}:${expires}`);
const qs = `pdm_path=${encodeURIComponent(folder)}&pdm_expires=${expires}&pdm_token=${folderToken}`;
console.log(`https://www.example.com${folder}master.m3u8?${qs}`);
console.log(`https://www.example.com${folder}720p/segment-000.ts?${qs}`);

For the exact-file case all three produce the same link, for example:

https://www.example.com/downloads/report.pdf?pdm_expires=1767225600&pdm_token=5f8a…64 hex chars…c31d
Checked at the edge

The signature is validated at the edge before any content is served: an unsigned or expired request on a protected path never reaches cached content or your origin.