A presigned URL is a link to one object in Amazon S3, or in an S3-compatible store, that carries a Signature Version 4 signature and an expiry in its query string, so whoever holds the link can perform one specific operation, such as GET or PUT, without credentials of their own.
"Signed URL" is the general idea. "Presigned URL" is S3's name for its version of it. This entry is part of the screenshot glossary. For the general idea and how any signature scheme is checked, read signed URL first.
Presigned URL vs signed URL
| S3 presigned URL | RenderScreenshot signed URL | A generic signed URL | |
|---|---|---|---|
| What it grants | One S3 operation on one object (GET, PUT, ...) | One screenshot render with fixed parameters | Whatever the issuing server decides |
| Signature | AWS SigV4: HMAC-SHA256 chain over method, path, query and signed headers | HMAC-SHA256 over the sorted request parameters, with your rs_secret_ key |
Usually an HMAC |
| Who checks it | S3 or the compatible service | The RenderScreenshot API | Your own server |
| Maximum lifetime | 7 days, and never longer than the credentials that signed it | 30 days; expires is required |
Your choice |
| Typical use | Let a browser or worker upload or download a file directly | Put a render URL in an <img> or og:image tag |
Paid downloads, private media |
The key point: a presigned URL is about storage. It moves bytes in or out of a bucket. A screenshot API signed URL is about rendering. It authorises the API to produce an image. They often meet in one pipeline: render, then store.
How it works technically
The SDK builds a canonical request from the method, the object path, the query and the headers it decides to sign. It hashes that, derives a signing key from your secret key plus the date, region and service, and adds the result to the URL. You can see the pieces in the query string:
X-Amz-Algorithm, X-Amz-Content-Sha256, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-Signature, X-Amz-SignedHeaders, x-id
That list is what our test script printed for a PUT URL. X-Amz-Credential contains the access key ID, date, region and service, but never the secret key. If a client changes any signed part (the method, the key, a signed header), S3 recomputes a different signature and refuses the request.
sequenceDiagram participant App as Your server (AWS credentials) participant R as Renderer (Puppeteer, Playwright or a worker) participant S3 as S3-compatible bucket App->>App: getSignedUrl(PutObject, expiresIn 900) App->>R: job with url + presigned PUT R->>R: render the page to PNG R->>S3: PUT bytes to the presigned URL (no credentials) S3->>S3: check signature and expiry S3-->>R: 200 App->>App: getSignedUrl(GetObject) for viewers
The renderer never holds bucket credentials. If it is compromised, the attacker gets one upload slot for one key for a few minutes.
How to set it with Puppeteer or Playwright
Browser libraries do not sign anything. They produce the bytes; the AWS SDK signs; fetch uploads. This script captures with Playwright and uploads through a presigned PUT:
import { chromium } from 'playwright'; import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; const endpoint = process.env.S3_ENDPOINT; // unset for AWS S3 const s3 = new S3Client({ region: process.env.S3_REGION ?? 'us-east-1', endpoint, forcePathStyle: Boolean(endpoint), // Without this, SDK v3 signs a CRC32 of an EMPTY body into presigned PUT URLs. requestChecksumCalculation: 'WHEN_REQUIRED', }); const Bucket = process.env.S3_BUCKET; const Key = 'captures/example.png'; const putUrl = await getSignedUrl(s3, new PutObjectCommand({ Bucket, Key, ContentType: 'image/png' }), { expiresIn: 900 }); const getUrl = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key }), { expiresIn: 3600 }); const browser = await chromium.launch(); let png; try { const page = await browser.newPage({ viewport: { width: 1200, height: 630 } }); await page.goto('https://example.com'); png = await page.screenshot(); } finally { await browser.close(); } // The uploader needs only the URL. Content-Type must match what was signed. const put = await fetch(putUrl, { method: 'PUT', body: png, headers: { 'Content-Type': 'image/png' } }); if (!put.ok) throw new Error(`PUT failed: ${put.status} ${await put.text()}`); const got = await fetch(getUrl); if (!got.ok) throw new Error(`GET failed: ${got.status}`); console.log(`PUT ${put.status}, GET ${got.status}, ${(await got.arrayBuffer()).byteLength} bytes`);
With Puppeteer, only the capture lines change: const png = await page.screenshot({ type: 'png' }) returns the same kind of bytes.
Tested on 2026-10-15 with Node 22.18.0, [email protected] and @aws-sdk/client-s3 3.1152.0, against a local moto 5.2.4 S3 server. The upload and the download both returned 200. The local server does not check signatures, so this run proves the flow, not the signature rules; check those against your provider.
Two details from that test:
- The checksum trap. Without
requestChecksumCalculation: 'WHEN_REQUIRED', SDK 3.1152.0 addedx-amz-checksum-crc32=AAAAAA==to the presigned PUT. That is the CRC32 of an empty body. A store that verifies it will refuse your real upload. - The 7-day cap is enforced in the SDK. Asking for 604,801 seconds failed before signing: "Signature version 4 presigned URLs must have an expiration date less than one week in the future".
Presigned URLs and RenderScreenshot
You do not need presigned URLs to get images out of RenderScreenshot. Every render returns an X-Cache-URL header, a public CDN link that needs no key and lives as long as the cache TTL. To put a render URL into public HTML, use our own signed URLs.
To keep a permanent copy in your own bucket, configure S3-compatible storage on a paid plan and add storage to the request. The API copies each fresh render to your bucket after it responds, using the access keys you saved in the dashboard. Your application can then hand out presigned GET URLs for those objects with the code above. The upload screenshots to S3 guide covers the setup.
Common mistakes
- Different Content-Type on upload. If
ContentTypewas signed, the PUT must send the same header. - Long expiries "to be safe". A leaked link works until it expires. Use minutes for uploads.
- Signing with short-lived credentials. A URL signed by a Lambda role dies when the role session ends, whatever
expiresInsays. - Virtual-host addressing on a custom endpoint. Many S3-compatible stores need
forcePathStyle: true. - Caching presigned GETs in a CDN by full URL. Every new signature is a new cache key. Serve public files from a public URL instead.
Related terms
- Signed URL: the general concept and the RenderScreenshot scheme.
- HMAC signature: the primitive SigV4 is built on.
- S3-compatible storage: where presigned URLs point.
- Cache TTL: how long
X-Cache-URLlinks stay valid.
To render a page and get a public link without managing a bucket, create a free account and read the X-Cache-URL header of your first response.