A signed URL is a link that contains its own permission: the request parameters, an expiry time and a signature computed with a secret key, so a server can trust the link without a login or an API key.
The person who holds the secret creates the link. Anyone who receives it can use it until it expires, but cannot change it. This entry is part of the screenshot glossary.
What it means in plain terms
A normal API call proves who you are with a header such as Authorization: Bearer .... That works between servers, but it breaks in places where you cannot add headers: an <img src>, an og:image tag, a link in an email. If you put the API key itself in such a URL, everyone who views the page can copy the key and spend your account.
A signed URL fixes this. Your server writes down exactly what the link is allowed to do and until when, then seals that with a signature only your secret could produce. The receiving server recomputes the seal. If one character changed, the seals do not match.
Cloud storage calls the same thing a presigned URL. CDNs use it for paid downloads. Screenshot APIs use it so an image URL can sit in public HTML.
The life of a signed URL
sequenceDiagram participant S as Your server (holds the secret) participant B as Browser or social crawler participant A as API or storage server S->>S: canonical string = sorted params + expires S->>S: signature = HMAC-SHA256(secret, string) S->>B: HTML with the signed link B->>A: GET link (no secret, no API key) A->>A: expired? recompute HMAC, compare in constant time A-->>B: 200 with the resource, or 401
How it works technically
Three rules make a signature scheme work:
- A canonical string. Signer and checker must build byte-identical input. Usually that means sorting parameters and encoding them one exact way. Most "invalid signature" bugs are encoding differences, not wrong keys.
- A keyed hash, usually an HMAC with SHA-256. Without the secret, nobody can compute a valid value for a changed string.
- An expiry inside the signed data. Because
expiresis part of the string, nobody can extend it.
The checker should compare signatures in constant time, so the response time does not leak how many leading characters matched. Here is a complete, generic sign-and-verify pair in Node.js:
// A generic HMAC signed URL: sign on one side, verify on the other. import { createHmac, timingSafeEqual } from 'node:crypto'; const SECRET = process.env.URL_SIGNING_SECRET ?? 'dev-only-secret'; export function sign(path, params, ttlSeconds) { const expires = Math.floor(Date.now() / 1000) + ttlSeconds; const query = new URLSearchParams({ ...params, expires: String(expires) }); query.sort(); // both sides must build the same canonical string const signature = createHmac('sha256', SECRET).update(`${path}?${query}`).digest('hex'); query.append('signature', signature); return `${path}?${query}`; } export function verify(signedPathAndQuery) { const url = new URL(signedPathAndQuery, 'http://placeholder'); const given = url.searchParams.get('signature') ?? ''; url.searchParams.delete('signature'); url.searchParams.sort(); if (Number(url.searchParams.get('expires')) < Date.now() / 1000) return 'expired'; const expected = createHmac('sha256', SECRET).update(`${url.pathname}?${url.searchParams}`).digest('hex'); const a = Buffer.from(given, 'hex'); const b = Buffer.from(expected, 'hex'); return a.length === b.length && timingSafeEqual(a, b) ? 'valid' : 'invalid'; } const link = sign('/files/report.pdf', { user: '42' }, 600); console.log(verify(link)); // valid console.log(verify(link.replace('user=42', 'user=43'))); // invalid console.log(verify(sign('/files/report.pdf', { user: '42' }, -1))); // expired
Tested on 2026-10-15 with Node 22.18.0. It printed valid, invalid, expired. This is a teaching example for your own endpoints. It is not the RenderScreenshot encoding, which differs in detail (see below).
Where Puppeteer and Playwright fit
Browser libraries do not sign anything. A signed URL is a delivery concern, not a rendering one. They meet in two places:
- When you run your own screenshot service on Puppeteer or Playwright, you need a scheme like the one above in front of it before you can put its URLs in public pages.
- When a page you capture loads assets from signed links, those links must still be valid at capture time. An expired asset link renders as a broken image in the screenshot.
Signed URLs in RenderScreenshot
RenderScreenshot accepts signed GET /v1/screenshot links. They carry a public key ID (rs_pub_...), an expires timestamp in Unix seconds and a signature, which is HMAC-SHA256 over the sorted, form-encoded parameters, made with your secret key (rs_secret_...). The secret never appears in the link. The maximum expiry is 30 days, and links without an expiry are not accepted.
A signed link looks like this (signature shortened):
https://api.renderscreenshot.com/v1/screenshot?expires=1792644628&key_id=rs_pub_...&preset=og_card&url=https%3A%2F%2Fexample.com%2Fblog%2Fhello-world&signature=5c0f...
The CLI builds one for you, reading the key pair from RS_PUBLIC_KEY_ID and RS_SECRET_KEY:
rs signed-url https://example.com/blog/hello-world --preset og_card --expires 7d
Tested signing code in six languages, the exact encoding rules and every error case are in the signed URLs reference.
Common mistakes
- Editing the link after signing. A template that appends
&format=webpbreaks the signature. Sign every parameter you send. - Encoding twice, or with a different encoder than the checker (
%20versus+). - Milliseconds instead of seconds in
expires. The timestamp lands thousands of years in the future, so the link seems to never expire. Divide by 1000. - Signing in the browser. The secret must stay on a server. Anything shipped to the client is public.
- Using a signed URL where a stored copy is better. A link that must work for years should point to your own file, not to a signed render.
Related terms
- HMAC signature
- Presigned URL
- Basic auth: the opposite approach, where credentials travel with each request.
- Twitter card and OG image: the tags where signed screenshot links usually end up.
To sign your first screenshot link, create a free account and request a signing key pair.