Documentation
The comprehensive guide to integrating the len.sh screenshot API into your development workflow.
Quick Start Guide
Get your API Key
Create an account at the signup page. Your unique API key is shown once — store it securely.
Make your first request
Call the /v1/screenshot endpoint with the url parameter.
Get your screenshot
The API returns the raw image binary. Use the endpoint URL directly as an img src or save to a file.
Authentication
All API requests (except /v1/health) require an API key. Two methods:
HTTP Header (Recommended)
Authorization: Bearer <key> Query Parameter
?access_key=YOUR_API_KEY Signed URLs
Signed URLs let you embed screenshots in client-side contexts — img tags, emails, social cards — without exposing your API key. Instead of authenticating with an API key, requests are verified using an HMAC-SHA256 signature computed with a separate signing secret.
How It Works
Collect all query parameters except signature.
Sort parameter names alphabetically.
Build the canonical string: key1=value1&key2=value2&... (use raw values, not URL-encoded).
Compute HMAC-SHA256(signing_secret, canonical_string) and output as hex.
Append &signature={hex} to the URL.
Example Signed URL
https://api.len.sh/v1/screenshot?key_id=abc123&url=https://example.com&signature=a1b2c3d4...
Security Notes
- check_circle The signing secret is separate from your API key
- check_circle The signing secret can be rotated independently
- check_circle Signed URLs do not expire (designed for static img tags)
- check_circle Rate limiting and monthly quotas still apply
All official SDKs include a signUrl helper that implements this algorithm for you.
Parameter Reference
All parameters work on both GET (query string) and POST (JSON body).
| Parameter | Type | Default | Description |
|---|---|---|---|
| url | string | — | Target URL to capture. Required* |
| html | string | — | Raw HTML to render (use POST). Required* |
| format | enum | png | Output format: png, jpeg, webp |
| width | int | 1280 | Viewport width in pixels (1–3840). |
| height | int | 720 | Viewport height in pixels (1–2160). |
| quality | int | 80 | JPEG/WebP quality (1–100, ignored for PNG). |
| full_page | bool | false | Capture the entire scrollable page. |
| selector | string | — | CSS selector to capture a specific element. |
| delay | int | 0 | Wait N ms before capture (0–10000). |
| wait_until | enum | load | load, domcontentloaded, networkidle0, networkidle2 |
| timeout | int | 15000 | Max wait time in ms (1–30000). |
| transparent | bool | false | Transparent background (PNG/WebP only). |
| user_agent | string | — | Custom user agent string. |
| device_scale | number | 1 | Device scale factor (1–3) for retina. |
| device | string | — | Device emulation preset (e.g. iphone-15, desktop-1080p). Sets width, height, scale, and user agent. |
| block_ads | bool | false | Hide common ad elements. |
| block_cookie_banners | bool | false | Hide cookie consent banners. |
| block_popups | bool | false | Hide marketing popups (newsletters, spin wheels, exit-intent overlays). |
| js | string | — | Custom JavaScript to inject after page load (max 10KB). |
| css | string | — | Custom CSS to inject (max 10KB). |
| cache_ttl | int | 86400 | Cache duration in seconds (0 = bypass, max 2592000). |
| response_type | enum | image | image (raw bytes) or json (metadata). |
*One of url or html is required.
Device Emulation Presets
Use the device parameter to emulate a specific device. This sets width, height, device scale, and user agent automatically. Explicit parameters override preset values.
| Device | Value | Width | Height | Scale |
|---|---|---|---|---|
| iPhone 15 | iphone-15 | 393 | 852 | 3x |
| iPhone 15 Pro Max | iphone-15-pro-max | 430 | 932 | 3x |
| iPad Pro | ipad-pro | 1024 | 1366 | 2x |
| Pixel 8 | pixel-8 | 412 | 915 | 2.625x |
| Samsung Galaxy S24 | samsung-s24 | 360 | 780 | 3x |
| MacBook Pro 16" | macbook-pro-16 | 1728 | 1117 | 2x |
| Desktop 1080p | desktop-1080p | 1920 | 1080 | 1x |
| Desktop 4K | desktop-4k | 3840 | 2160 | 1x |
OG Image Generation
Generate branded Open Graph images (1200×630) from structured parameters. Uses the same authentication as the screenshot endpoint.
/v1/og Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| title | string | — | Main heading (max 200 chars, clamped to 3 lines). Required |
| subtitle | string | — | Secondary text (max 300 chars). |
| badge | string | — | Pill badge text e.g. "12 items" (max 50 chars). |
| url | string | — | URL displayed in footer (max 200 chars). |
| brand_name | string | — | Brand name in footer (max 100 chars). |
| brand_color | string | #4850e5 | Accent color, hex format. |
| theme | enum | dark | dark or light. |
| format | enum | png | Output format: png, jpeg, webp. |
| quality | int | 90 | Image quality (1–100). |
| cache_ttl | int | 86400 | Cache duration in seconds (0 = bypass, max 2592000). |
Response
Returns raw image bytes with the following headers:
Rate limit and quota headers are also included, identical to the screenshot endpoint.
PDF Generation
Generate PDFs from any URL or HTML content. Uses the same authentication, caching, and rate limiting as the screenshot endpoint.
/v1/pdf Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| url | string | — | Target URL to render as PDF. Required* |
| html | string | — | Raw HTML to render (use POST). Required* |
| paper_size | enum | A4 | Page size: A4, Letter, Legal, Tabloid, Ledger. |
| landscape | bool | false | Use landscape orientation. |
| scale | float | 1.0 | Scale factor (0.1–2.0). |
| margin_top | string | 0.4in | Top margin (CSS unit). |
| margin_right | string | 0.4in | Right margin (CSS unit). |
| margin_bottom | string | 0.4in | Bottom margin (CSS unit). |
| margin_left | string | 0.4in | Left margin (CSS unit). |
| print_background | bool | true | Include background colors/images. |
| page_ranges | string | — | Page ranges to include, e.g. "1-3, 5". |
| header_template | string | — | HTML template for page headers. |
| footer_template | string | — | HTML template for page footers. |
| wait_until | enum | load | load, domcontentloaded, networkidle0, networkidle2 |
| timeout | int | 15000 | Max wait time in ms (1–30000). |
| delay | int | 0 | Wait N ms before rendering (0–10000). |
| user_agent | string | — | Custom user agent string. |
| device | string | — | Device emulation preset for user agent (e.g. iphone-15). |
| block_ads | bool | false | Hide common ad elements. |
| block_cookie_banners | bool | false | Hide cookie consent banners. |
| block_popups | bool | false | Hide marketing popups (newsletters, spin wheels, exit-intent overlays). |
| js | string | — | Custom JavaScript to inject after page load (max 10KB). |
| css | string | — | Custom CSS to inject (max 10KB). |
| cache_ttl | int | 86400 | Cache duration in seconds (0 = bypass, max 2592000). |
| response_type | enum | pdf (raw bytes) or json (metadata). |
*One of url or html is required.
Response
Returns raw PDF bytes with the following headers:
Use response_type=json to receive metadata (file URL, size, cache status) instead of the raw PDF.
Error Codes
| HTTP | Code | Description |
|---|---|---|
| 400 | MISSING_URL | Neither url nor html provided |
| 400 | INVALID_URL | URL is malformed or uses a blocked protocol |
| 400 | INVALID_PARAMETER | Parameter value out of range |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 429 | RATE_LIMITED | Too many requests per hour |
| 429 | QUOTA_EXCEEDED | Monthly screenshot quota exceeded |
| 502 | RENDER_FAILED | Browser Rendering returned an error |
| 504 | RENDER_TIMEOUT | Screenshot exceeded timeout |
Rate Limits
Rate limits are enforced per API key on a rolling hourly window. The default limit is 100 requests per hour.
Response Headers
Every API response includes rate limit headers so you can track your usage:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Total requests allowed per hour |
| X-RateLimit-Remaining | Requests remaining in the current hour |
| X-RateLimit-Reset | Unix timestamp when the hourly window resets |
| X-Quota-Limit | Total screenshots allowed per month |
| X-Quota-Remaining | Screenshots remaining this billing month |
| X-Quota-Reset | Unix timestamp when the monthly quota resets (1st of next month) |
When the hourly limit is exceeded, you'll receive a 429 RATE_LIMITED response. Wait until the X-RateLimit-Reset timestamp and retry.
Monthly Quotas
Each plan includes a monthly screenshot quota. When the quota is exceeded, requests return a 429 QUOTA_EXCEEDED error until the next billing month.
| Plan | Screenshots / Month | Rate Limit / Hour | Price |
|---|---|---|---|
| Free | 100 | 100 | €0 |
| Pro | 25,000 | 100 | Coming soon |
| Scale | 500,000 | 100 | €99/mo |
| Enterprise | 100,000+ | Custom | Custom |
Quota Exceeded Response
When your monthly quota is exceeded, the API returns:
Use the X-Quota-Remaining header to track usage and the X-Quota-Reset header to know when the quota resets. If you need more volume, upgrade your plan.
Handling Rate Limits
Best practices for building reliable integrations that respect rate limits.
1. Check Headers Proactively
Monitor the remaining count on every response instead of waiting for a 429 error.
const res = await fetch(url, { headers });
const remaining = res.headers.get("X-Quota-Remaining");
if (remaining !== null && parseInt(remaining) < 10) {
console.warn(`Low quota: ${remaining} screenshots remaining`);
} 2. Retry with Exponential Backoff
When you receive a 429, wait and retry with increasing delays.
async function captureWithRetry(url, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(
"https://api.len.sh/v1/screenshot?url=" + encodeURIComponent(url),
{ headers: { Authorization: "Bearer YOUR_API_KEY" } }
);
if (res.status !== 429) return res;
const reset = res.headers.get("X-RateLimit-Reset");
const waitMs = reset
? Math.max(0, parseInt(reset) * 1000 - Date.now())
: Math.pow(2, attempt) * 1000;
await new Promise((r) => setTimeout(r, waitMs));
}
throw new Error("Rate limit retries exhausted");
} 3. Use Caching
Avoid redundant captures by using the built-in cache. Cached responses (cache HIT) still count against hourly rate limits, but they don't re-render the page.
// Cache for 24 hours (default) /v1/screenshot?url=https://example.com&cache_ttl=86400 // Cache for 7 days /v1/screenshot?url=https://example.com&cache_ttl=604800
4. Distinguish Error Types
Handle hourly rate limits and monthly quotas differently:
- schedule
RATE_LIMITED— Wait untilX-RateLimit-Reset(usually minutes). Retry the request. - block
QUOTA_EXCEEDED— Monthly limit reached. Either upgrade your plan or wait until the next billing month (X-Quota-Reset).
Caching
Screenshots are cached in Cloudflare R2. The cache key is a SHA-256 hash of all rendering-affecting parameters.
- check_circle Default TTL: 24 hours (86400 seconds)
- check_circle Bypass cache: Set
cache_ttl=0 - check_circle Max TTL: 30 days (2592000 seconds)
- check_circle Cache HIT: ~50-100ms response time
Check the X-Len-Cache response header to see if a response was served from cache.