A bearer token is a credential sent in the Authorization: Bearer <token> HTTP header that grants access to whoever presents it, with no further proof of identity.
The name comes from the bearer bond: the holder of the paper owns it. The scheme is defined in RFC 6750, written for OAuth 2.0, and is now the default way to authenticate API calls. This entry is part of the screenshot glossary.
What it means in plain terms
A bearer token works like a ticket, not like a passport. The server does not ask who you are. It checks that the ticket is valid and lets you in. That makes bearer tokens simple to use from any language and any tool, and it makes a leaked token as good as a stolen password until it expires or is revoked.
For screenshots, bearer tokens appear in two places:
- Calling a screenshot API. Your server sends its API key as a bearer token.
- Capturing a protected page. The headless browser must present a token to the site you capture, for example an internal dashboard behind an API gateway.
How it works technically
your code / headless browser server ──────────────────────────── ────── GET /reports/q3 Authorization: Bearer eyJhbGciOi... ───────▶ look up or verify token │ valid → 200 + page │ invalid → 401 ▼ WWW-Authenticate: Bearer
The token itself is opaque to the client. It can be a random string the server looks up in a database, or a signed JWT the server verifies without a lookup. Either way:
- It travels in a header, so it stays out of URLs, browser history and most access logs.
- It must only travel over HTTPS. Anyone who reads the header can replay it.
- It has no binding to the sender. Unlike a signed URL or an HMAC signature, it does not prove that the request was not changed.
How to send one from a headless browser
The trap in every tool is scope. A real browser never adds your Authorization header to requests for other hosts, but an automation setting often does. We tested this with two local origins: a page on port 4101 that requires the token and loads an image from port 4102. With the scoped code below, port 4101 received Bearer test-token-123 and port 4102 received no Authorization header.
Puppeteer
page.setExtraHTTPHeaders() sends the header to every host the page contacts. Use request interception to limit it to the target origin:
import puppeteer from 'puppeteer'; const target = new URL(process.argv[2] ?? 'https://example.com/'); const token = process.env.TARGET_TOKEN; if (!token) throw new Error('Set TARGET_TOKEN'); const browser = await puppeteer.launch(); try { const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (req) => { if (req.isInterceptResolutionHandled()) return; const headers = { ...req.headers() }; if (new URL(req.url()).origin === target.origin) { headers.authorization = `Bearer ${token}`; } req.continue({ headers }); }); const res = await page.goto(target.href, { waitUntil: 'networkidle0' }); if (!res?.ok()) throw new Error(`Target answered ${res?.status()}`); await page.screenshot({ path: 'bearer-puppeteer.png' }); } finally { await browser.close(); }
Playwright
extraHTTPHeaders on a context has the same problem. A route that matches only the target origin is the clean fix:
import { chromium } from 'playwright'; const target = new URL(process.argv[2] ?? 'https://example.com/'); const token = process.env.TARGET_TOKEN; if (!token) throw new Error('Set TARGET_TOKEN'); const browser = await chromium.launch(); try { const context = await browser.newContext(); await context.route(`${target.origin}/**`, (route) => route.continue({ headers: { ...route.request().headers(), authorization: `Bearer ${token}` }, }) ); const page = await context.newPage(); const res = await page.goto(target.href, { waitUntil: 'networkidle' }); if (!res?.ok()) throw new Error(`Target answered ${res?.status()}`); await page.screenshot({ path: 'bearer-playwright.png' }); } finally { await browser.close(); }
Tested on 2026-10-15 with Node 22.18.0, [email protected] and [email protected], against the local two-origin server and against https://example.com. The Puppeteer run used headless: 'shell' because full headless mode stalled on screenshots on the test machine.
RenderScreenshot
Here the two uses separate cleanly. Your API key authenticates you to the API as a bearer token, as described in bearer token authentication. A token for the page you capture goes in network.auth:
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://dashboard.example.com/reports/q3", "network": { "auth": { "type": "bearer", "token": "'"$REPORT_TOKEN"'" } } }' \ --output q3.png
In the Node.js SDK the same request is TakeOptions.url(...).authBearer(process.env.REPORT_TOKEN). See network options. For a dashboard that uses cookies instead of a header, read the authenticated pages guide. The API requests on this page were not run for it.
Common mistakes
- Leaking the token to third parties with a global header setting. Scope it to one origin, as above.
- Putting the token in a query string because it is easier. URLs end up in logs and
Refererheaders. For public image links, use a signed URL instead of?api_key=. - Using a long-lived token for captures. Mint a short-lived, read-only token for the screenshot job where the site allows it.
- Lowercase
bearer. The scheme name is case-insensitive in the standard, but some gateways only acceptBearer. - Treating a 200 as success. Many apps answer an expired token with a login page and a 200. Check the content, not only the status.
Related terms
- Basic auth: username and password in a header.
- Signed URL: a link that carries its own permission.
- HMAC signature: how signed URLs prove they were not changed.
- Browser context: where cookies and headers live in a headless browser.
To capture a token-protected page without running a browser, create a free account and send the request above.