Browser API
Screenshot
Capture a page in a real browser as PNG, JPEG or WebP: the viewport, the whole page or one element, on a desktop or a phone, with cookie banners hidden. One capture per request, one price.
Body parameters
| Name | Type | Description |
|---|---|---|
urlrequired | string (≤ 2048 chars) | Absolute http:// or https:// URL. No embedded credentials, no private or internal hosts. |
full_page | boolean | Capture the whole scrollable page instead of the viewport, cut at max_height. Default: false. |
max_height | integer (100–16384) | Height cap in CSS pixels for full_page and selector captures. Never more than 16,384 device pixels: 16,384 CSS px at 1x, a third of that on a 3x phone. A taller page is cut and the response says clipped. The price is the same either way. |
selector | string | CSS selector of one element to capture (the first visible match), e.g. "#pricing". 422 selector_not_found, not charged, when nothing visible matches within 5 s of the page loading. Not with full_page. |
device | string | Device preset (below): viewport, pixel ratio, touch, mobile layout and user agent together. Not with viewport. |
viewport | { width, height } | Viewport in CSS pixels: width 320–3840, height 240–4320. Both are required when you send it. |
format | "png" | "jpeg" | "webp" | Image format. WebP is the smallest. type is accepted as an older name. Default: "png". |
quality | integer (1–100) | JPEG and WebP quality. Default: 80. |
omit_background | boolean | Transparent background where the page paints none (png and webp). Default: false. |
hide_selectors | string[] (up to 20) | CSS selectors hidden (display: none) right before the capture: cookie banners, chat widgets, promo bars. Selectors that match nothing are ignored; an invalid one is a 400. |
dark_mode | boolean | Emulate prefers-color-scheme: dark, for sites that follow the system theme. Default: false. |
unblock | "none" | "stealth" | "residential" | "auto" | Unblocking tier. residential goes through your team's own proxy. auto escalates none → stealth → residential (when your team has a proxy) and charges only for the tier that succeeded. See Unblocking tiers. Default: "none". |
timeout_ms | integer (1000–120000) | Time budget for the whole request, shared by every unblock tier tried. Running out returns 504 timeout (not charged). Default: 30000. |
max_age | integer (seconds, 0–86400) | Opt-in cache. If your account made the same request (same URL and options) within max_age seconds, you get that response back without us contacting the site, for 0 credits. An older copy is revalidated with a conditional request when the site supports it. 0 always fetches but stores the result. Omit it and the cache is not used. See Render → Caching. |
respect_robots | boolean | Check the site’s robots.txt (user agent PageteerBot) first. A disallowed URL fails with 403 robots_disallowed and is not charged; a Crawl-delay spaces our requests to that site. Default: false. |
actions | object[] (≤ 20) | Read-only page actions run in the browser after the page loads: wait, wait_for, scroll, click ("Load more", tabs, accordions), press and screenshot. Forces a browser render. No typing, form submission or logins. See Page actions. |
Examples
curl -X POST https://api.104-236-84-15.sslip.io/v1/screenshot \
-H "Authorization: Bearer $PAGETEER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","full_page":true,"format":"png"}' \
-o example.pngconst res = await fetch('https://api.104-236-84-15.sslip.io/v1/screenshot', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAGETEER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://example.com",
"full_page": true,
"format": "png"
}),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('example.png', bytes));import os
import requests
headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.post(
"https://api.104-236-84-15.sslip.io/v1/screenshot",
headers=headers,
json={
"url": "https://example.com",
"full_page": True,
"format": "png",
},
timeout=120,
)
res.raise_for_status()
with open("example.png", "wb") as f:
f.write(res.content)An iPhone screenshot of one section, as WebP, with the cookie banner hidden and the dark theme on:
curl -X POST https://api.104-236-84-15.sslip.io/v1/screenshot \
-H "Authorization: Bearer $PAGETEER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/pricing","device":"iphone-15","selector":"#plans","hide_selectors":["#onetrust-banner-sdk",".cookie-notice"],"dark_mode":true,"format":"webp","quality":80}' \
-o plans.webpconst res = await fetch('https://api.104-236-84-15.sslip.io/v1/screenshot', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAGETEER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://example.com/pricing",
"device": "iphone-15",
"selector": "#plans",
"hide_selectors": [
"#onetrust-banner-sdk",
".cookie-notice"
],
"dark_mode": true,
"format": "webp",
"quality": 80
}),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('plans.webp', bytes));import os
import requests
headers = {"Authorization": f"Bearer {os.environ['PAGETEER_API_KEY']}"}
res = requests.post(
"https://api.104-236-84-15.sslip.io/v1/screenshot",
headers=headers,
json={
"url": "https://example.com/pricing",
"device": "iphone-15",
"selector": "#plans",
"hide_selectors": ["#onetrust-banner-sdk", ".cookie-notice"],
"dark_mode": True,
"format": "webp",
"quality": 80,
},
timeout=120,
)
res.raise_for_status()
with open("plans.webp", "wb") as f:
f.write(res.content)Device presets
A preset sets the viewport, the device pixel ratio (the image is that many times larger), touch, the mobile layout and a matching user agent: Safari on iPhone and iPad, Chrome on Android and desktop.
| device | Device | Viewport (CSS px) | Pixel ratio |
|---|---|---|---|
iphone-se | iPhone SE | 375 × 667 | 2x |
iphone-15 | iPhone 15 | 393 × 852 | 3x |
iphone-15-pro-max | iPhone 15 Pro Max | 430 × 932 | 3x |
pixel-8 | Pixel 8 | 412 × 915 | 2.625x |
galaxy-s24 | Galaxy S24 | 360 × 780 | 3x |
ipad | iPad (10th gen) | 820 × 1180 | 2x |
ipad-pro | iPad Pro 12.9" | 1024 × 1366 | 2x |
laptop-1366 | Laptop 1366×768 | 1366 × 768 | 1x |
macbook-pro | MacBook Pro 14" (Retina) | 1512 × 982 | 2x |
desktop-1080p | Desktop 1920×1080 | 1920 × 1080 | 1x |
desktop-1440p | Desktop 2560×1440 | 2560 × 1440 | 1x |
Response
By default the body is the image itself (image/png, image/jpeg or image/webp). The x-tier-used and x-target-status headers say which tier captured it and what the site answered, and x-screenshot-clipped: true that a full-page or element capture was cut at max_height, next to the usual x-credits-used, x-credits-remaining and x-request-id.
Send Accept: application/json to get JSON with the image as base64 instead (what you want in serverless functions, queues or anything that only handles JSON):
{
"url": "https://example.com",
"final_url": "https://example.com/",
"status": 200,
"content_type": "image/png",
"screenshot_base64": "iVBORw0KGgoAAAANSUhEUgAABQAAAAMgCAIAAADz…",
"clipped": false,
"tier_used": "none",
"attempts": [{ "tier": "none", "status": 200, "ms": 905 }],
"cached": false,
"credits": 5
}status is the site's HTTP status. With max_age, cached, cache_age_s and revalidated work as for render.
Hiding cookie banners. Consent banners usually live in a fixed element with a stable id or class: #onetrust-banner-sdk (OneTrust), #CybotCookiebotDialog (Cookiebot), .fc-consent-root (Funding Choices). Hiding one does not accept or reject anything: no click is made and no script of yours runs.
Credits
5 credits, plus the tier that succeeded (stealth +5, residential through your own proxy +0): a stealth screenshot costs 10. Devices, formats, element captures, hidden elements, dark mode and full pages cost the same; very tall pages are cut at max_height instead of costing more. A target 5xx is captured but not charged; cache hits cost 0; selector_not_found is free.
Errors
| Status | Code | When | Charged | What to do |
|---|---|---|---|---|
| 400 | invalid_request | A parameter is missing, invalid or unknown (the message and error.issues name it). | No | Fix the request; retrying it unchanged fails again. |
| 401 | invalid_api_key | No key, an unknown key, or a revoked or expired key. | No | Check the Authorization header and the key on the API keys page. |
| 402 | insufficient_credits | Not enough credits left this month for this request, and billable usage is off. error.reason says why: free_credits_used, payment_failed or subscription_inactive. | No | Act on the reason at error.billing_url: add a card, update the payment method, or restart the subscription. Or wait for the 1st (UTC). |
| 402 | spend_limit_reached | Billable usage is on and this month’s spend limit is reached (error.reason: spend_limit). | No | Raise the spend limit at error.billing_url (the Billing page), or wait for the 1st (UTC). |
| 403 | forbidden_target | The URL is denylisted, resolves (or redirects) to a private or internal address, or sends cookies or credentials to a login, account or checkout page. | No | Do not retry. See the Acceptable Use Policy. |
| 403 | robots_disallowed | respect_robots is on (default for crawl and batch) and the site’s robots.txt disallows the URL for PageteerBot. | No | Skip the URL. Turn respect_robots off only for sites you own or may crawl. |
| 403 | org_suspended | The account is suspended. | No | Contact support. |
| 403 | feature_not_in_plan | Schema extraction on the free mode or while it is not available on this deployment. | No | Use selectors or structured, or add a card. |
| 403 | proxy_not_configured | unblock "residential" without a team proxy saved in Settings → Proxy (404 from POST /v1/proxy/test). | No | Save your proxy URL in Settings → Proxy (see Bring your own proxy), or use another tier. |
| 413 | payload_too_large | The request body is over 64 KB (8 MB for POST /v1/batch), or a crawl is too large to export in one file. | No | Send less, or split a batch; read a large crawl with GET /v1/crawl/:id. |
| 422 | target_blocked | The site blocked every tier we tried (with unblock "auto", the tiers this deployment offers). block_reason, block_vendor and attempts say how. | No | Try a stronger unblock tier (or "auto"); if auto already failed, retry much later. |
| 422 | captcha_required | The site answered with a CAPTCHA or bot challenge. We never solve them and try no further tier. | No | Do not retry soon. Use the site’s API or data feed if it has one. |
| 422 | login_required | The site redirected to a login or sign-in page (final_url says where). We never log in. | No | The content is not public: skip it. |
| 429 | rate_limited | Over your requests per minute, or too many requests to one site across the platform right now. | No | Wait Retry-After seconds, then back off with jitter. |
| 429 | concurrency_limit | Too many of your requests are in flight at once. | No | Wait for one to finish (Retry-After: 1) or lower your parallelism. |
| 429 | target_rate_limited | The site asked us to slow down (429, or 503 with Retry-After), or per-site pacing would not let your request start in time. We pause that site for everyone. | No | Wait error.retry_after seconds (also in Retry-After). |
| 502 | proxy_failed | Your proxy failed: reason "auth" (it rejected the credentials), "unreachable", "status" (it refused the connection) or "blocked" (its host resolves to a private address). | No | Fix the URL or credentials in Settings → Proxy and test it. "unreachable" and "status" may pass on retry. |
| 500 / 502 / 503 | internal_error | Something failed on our side (rendering engine, LLM extraction or the API itself), or the rendering engine is at capacity (503, Retry-After: 5). | No | Retry with backoff (honor Retry-After); quote x-request-id if it persists. |
| 504 | timeout | The site did not answer within timeout_ms. | No | Retry, maybe with a larger timeout_ms or mode "http". |
| 422 | selector_not_found | A screenshot selector matched no visible element within 5 seconds of the page loading. Not charged. | No | Check the selector against the live page (it may differ by device or need a consent click), or capture the page instead. |