# localscreenshot documentation > Take a screenshot of a public web page, from an AI tool (MCP), a command line or a REST API. Every page of https://docs.localscreenshot.com in one text. --- Source: https://docs.localscreenshot.com/overview # Overview localscreenshot takes a screenshot of a public web page and gives you the image. Ask your AI for it, or call the API yourself. It is the same service either way. - **Ask your AI**: No code. Connect once, then ask in plain words. - **Command line**: One curl command. - **From your code**: One HTTP request, any language. ## What you can do - Take a screenshot of a public page, as a desktop or as a phone sees it. - Capture the whole page, or only one element. - Remove most cookie banners, ads, chat bubbles and pop-ups before the picture is taken. - Save the page as `png`, `jpeg`, `webp` or `pdf`. - Load the page from one of 227 countries, with a signed technical attestation of the country we observed. ## Three ways in All three use the same API key and the same credits. | Way in | For whom | Where to start | | --- | --- | --- | | Your AI tool (MCP) | Anyone who works with Claude Code, Cursor, Codex and similar tools | [Ask your AI (MCP)](https://docs.localscreenshot.com/agent) | | Command line | Scripts and quick checks, with curl | [Quickstart](https://docs.localscreenshot.com/quickstart) | | REST API | Your own code, in any language | [Take a screenshot](https://docs.localscreenshot.com/take) | ## How a screenshot goes 1. You send a page address, with the options you want. 2. We open the page in a real browser, wait for it to load and take the picture. 3. You get the image back, or a clear error. A failed screenshot is not charged. ## What it costs - A screenshot uses 1 credit. From a chosen country, it uses 2. - Your account starts with 50 free credits. They do not expire. - More on [Credits and limits](https://docs.localscreenshot.com/credits). > **What it does not do.** It does not sign in to websites, solve CAPTCHAs or get past paywalls. It only reads public pages: it never fills in a form. When a site shows a bot check instead of its page, you get the error `challenge_page` instead of a picture. ## For AI tools and agents - Every page of these docs exists as Markdown: add `.md` to its address. - The whole documentation in one text: `llms-full.txt`, linked in the menu. - The API described for machines: [openapi.json](https://api.localscreenshot.com/v1/openapi.json). **Where things are** ```text API https://api.localscreenshot.com/v1 MCP server https://api.localscreenshot.com/mcp Dashboard https://localscreenshot.com/dashboard OpenAPI https://api.localscreenshot.com/v1/openapi.json ``` **Your first screenshot** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o shot.png ``` --- Source: https://docs.localscreenshot.com/quickstart # Quickstart Take your first screenshot in about a minute. There are three ways in. Pick the one that suits you: they all do the same thing. - **Ask your AI**: No code. Connect once, then ask in plain words. - **Command line**: One curl command. - **From your code**: One HTTP request, any language. ## 1. Get your API key Create a key in the [dashboard](https://localscreenshot.com/dashboard), under **Connect**. It starts with `lss_live_`. You can copy it again from there any time. Keep it secret. Your account comes with 50 free credits. The examples on this page read the key from an environment variable, so it is not written in every command or saved in a file you share: ```bash export LOCALSCREENSHOT_API_KEY="your key" ``` ## 2. Take a screenshot ### Ask your AI Add our MCP server to your AI tool, with your key in the `Authorization` header. [Ask your AI (MCP)](https://docs.localscreenshot.com/agent) has the exact line for each tool. Then just ask: > "Take a screenshot of wikipedia.org on an iPhone." Your AI gets three tools: `take_screenshot`, `list_countries` and `get_credits`. **MCP server** ```text Address https://api.localscreenshot.com/mcp Transport HTTP Header Authorization: Bearer $LOCALSCREENSHOT_API_KEY ``` ### Or call the API Send the page address to `/v1/screenshot`. The answer is the image itself. Your key goes in the `Authorization` header, never in the address. **Take a screenshot** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o shot.png ``` **Response headers** ```http HTTP/1.1 200 OK Content-Type: image/png X-Screenshot-Id: 01JABCDEF2G3H4J5K6M7N8P9QR X-Credits-Charged: 1 X-Credits-Remaining: 49 X-Page-Status: 200 ``` ## 3. Add what you need Every option is one parameter. A few to start with: | Option | What it does | | --- | --- | | `device=iphone_15` | Capture the page as a phone sees it. | | `full_page=true` | Capture the whole page, not only the visible part. | | `block_cookie_banners=true` | Remove most cookie banners before the picture. | | `country=DK` | Load the page from a country. 227 to choose from. | | `format=pdf` | Save the page as a PDF. Also `png`, `jpeg`, `webp`. | | `response=json` | Get a link to the image and the page details instead of the image. | The full list is on [Take a screenshot](https://docs.localscreenshot.com/take). **With response=json** ```json { "id": "01JABCDEF2G3H4J5K6M7N8P9QR", "status": "succeeded", "created_at": "2026-10-08T10:47:02Z", "finished_at": "2026-10-08T10:47:06Z", "image": { "url": "https://api.localscreenshot.com/v1/screenshots/01JABC.../image?expires=...&signature=...", "mime": "image/png", "bytes": 83672, "width": 1440, "height": 900, "pages": null, "sha256": "a5b9081b8430...", "truncated": false, "available_until": "2026-10-15T10:47:02Z" }, "page": { "status": 200, "final_url": "https://www.wikipedia.org/", "title": "Wikipedia", "redirects": [] }, "country": null, "attestation": null, "credits": { "charged": 1, "reserved": 1, "remaining": 49 }, "error": null, "links": { "self": "https://api.localscreenshot.com/v1/screenshots/01JABC..." } } ``` ## What it costs - A screenshot uses 1 credit. From a chosen country, it uses 2. - A failed screenshot is not charged. The number of failed screenshots per day is limited. - Your 50 signup credits do not expire. Monthly credits reset each month. Pack credits do not expire. > **What it does not do.** It does not sign in to websites, solve CAPTCHAs or get past paywalls. You get a clear error instead. --- Source: https://docs.localscreenshot.com/agent # Ask your AI (MCP) Connect your AI tool once, then ask for screenshots in plain words. No code. It works through MCP, the standard way AI tools reach outside services. ## What you need - An API key. Create one in the [dashboard](https://localscreenshot.com/dashboard), under **Connect**. - An AI tool that can add a remote MCP server over HTTP and send a header with it. Every tool needs the same two things: the address of our server, and your key in the `Authorization` header. There is no sign-in window to go through. **MCP server** ```text Address https://api.localscreenshot.com/mcp Transport HTTP Header Authorization: Bearer $LOCALSCREENSHOT_API_KEY ``` ## Connect your tool The lines below read your key from the environment variable `LOCALSCREENSHOT_API_KEY` where the tool allows it. Set it first: ```bash export LOCALSCREENSHOT_API_KEY="your key" ``` The dashboard shows the same lines with your own key already in place, ready to copy. ### Claude Code One command in your terminal. ```bash claude mcp add --transport http localscreenshot https://api.localscreenshot.com/mcp \ --header "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` ### Cursor Paste into .cursor/mcp.json. Cursor reads the key from your environment. ```json { "mcpServers": { "localscreenshot": { "url": "https://api.localscreenshot.com/mcp", "headers": { "Authorization": "Bearer ${env:LOCALSCREENSHOT_API_KEY}" } } } } ``` ### Codex One command in your terminal. Codex reads the key from your environment. ```bash # Codex sends it as "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" codex mcp add localscreenshot --url https://api.localscreenshot.com/mcp \ --bearer-token-env-var LOCALSCREENSHOT_API_KEY ``` ### Gemini CLI One command in your terminal. ```bash gemini mcp add --transport http \ --header "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ localscreenshot https://api.localscreenshot.com/mcp ``` ### VS Code Paste into .vscode/mcp.json. VS Code asks for your key once and keeps it. ```json { "inputs": [ { "type": "promptString", "id": "localscreenshot-key", "description": "localscreenshot API key", "password": true } ], "servers": { "localscreenshot": { "type": "http", "url": "https://api.localscreenshot.com/mcp", "headers": { "Authorization": "Bearer ${input:localscreenshot-key}" } } } } ``` ### Cline Paste into cline_mcp_settings.json, with your key in place of the variable. This tool does not read environment variables in its settings. ```json { "mcpServers": { "localscreenshot": { "type": "streamableHttp", "url": "https://api.localscreenshot.com/mcp", "headers": { "Authorization": "Bearer $LOCALSCREENSHOT_API_KEY" } } } } ``` ### Zed Paste into your Zed settings, with your key in place of the variable. This tool does not read environment variables in its settings. ```json { "context_servers": { "localscreenshot": { "url": "https://api.localscreenshot.com/mcp", "headers": { "Authorization": "Bearer $LOCALSCREENSHOT_API_KEY" } } } } ``` ### Other clients For a client that takes a server address and a header. ```text Address: https://api.localscreenshot.com/mcp Header: Authorization: Bearer $LOCALSCREENSHOT_API_KEY ``` ### Not ready yet We have not checked how these connect, so we show no command for them: ChatGPT, Claude, Copilot, Windsurf, Replit, v0, Perplexity, Le Chat, DeepSeek, Kimi, Qwen, n8n, Make, Zapier, LangChain, CrewAI. ## Then ask > "Take a screenshot of wikipedia.org on an iPhone." A few more things you can say: - "Take a full page screenshot of example.com without the cookie banner." - "Save example.com as a PDF." - "Show me only the pricing table of example.com/pricing." - "How many credits do I have left?" ## What your AI gets Three tools: | Tool | What it does | | --- | --- | | `take_screenshot` | Takes the screenshot and returns the image, the page title, its final address and status, and the credits left. | | `list_countries` | Lists the countries a screenshot can be taken from. | | `get_credits` | Says how many credits are left. | `take_screenshot` accepts the same options as the API, listed on [Take a screenshot](https://docs.localscreenshot.com/take), except `async` and `response`: the server waits for the picture and decides how to hand it back. - The image is scaled down to 1,568 pixels wide unless you ask for another `max_width`. That keeps it readable for a vision model without spending tokens for nothing. - A PDF is not shown as a picture: your AI gets a short-lived link to download it. - A very large image (over 4 MB) is not attached either: your AI gets its link. - An error comes back as a plain sentence with its code, so your AI can tell you what happened. See [Errors](https://docs.localscreenshot.com/errors). ## What it costs The same as the API: 1 credit for a screenshot, 2 from a chosen country. A failed screenshot is not charged. The limits are per account, whichever way you come in. > **What it does not do.** Your AI cannot use it to sign in to a website, solve a CAPTCHA or get past a paywall. It reads public pages only. --- Source: https://docs.localscreenshot.com/auth # Your API key Every request carries your API key. The same key works for the API and for the MCP server. ## Send your key Put it in the `Authorization` header, after the word `Bearer`. **The header** ```http Authorization: Bearer $LOCALSCREENSHOT_API_KEY ``` - The key is only read from that header. A key written in the address is not accepted: an address ends up in logs, in browser history and in `Referer` headers. - A missing or wrong key gets the error `unauthorized`, with status 401. - Too many wrong keys from the same IP address are slowed down with `rate_limited`. **Check that your key works** ```bash curl "https://api.localscreenshot.com/v1/credits" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` **200** ```json { "credits": { "remaining": 50 }, "costs": { "base": 1, "surcharges": { "country": 1 } }, "limits": { "requests_per_minute": 10, "concurrent_screenshots": 2 } } ``` ## Create and copy a key Keys are made in the [dashboard](https://localscreenshot.com/dashboard), under **Connect**. - A key starts with `lss_live_`, followed by 40 letters and digits. - The dashboard shows only the start of a key. Its **Copy** button gives you the whole key again, any time. - You can have up to 10 active keys. A key you have just rotated keeps working for its last 24 hours on top of those. Give each key a name that says where it is used. ## Rotate a key Rotating a key gives you a new one and keeps the old one working for 24 hours, so you have time to replace it where it is used. After that the old key stops. ## Revoke a key Revoking a key stops it right away. Use it when a key may have leaked. ## Keep it secret - Read the key from an environment variable or a secret store. Do not write it in code you share. - Never call the API from a web page or a mobile app: anyone could read the key there. Call it from your server. - Credits and limits belong to the account, not to a key. Making more keys does not raise a limit. > **A key works without a password.** Anyone who has it can spend your credits. If you pasted one somewhere public, revoke it and create another. --- Source: https://docs.localscreenshot.com/take # Take a screenshot `GET` `POST` `/v1/screenshot` Takes a screenshot of a public web page and waits for it. By default the answer is the image. With `GET`, options go in the address. With `POST`, they go in a JSON body. **GET, options in the address** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org&device=iphone_15&full_page=true" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o shot.png ``` **POST, options in a JSON body** ```bash curl -X POST "https://api.localscreenshot.com/v1/screenshot" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -H "Idempotency-Key: order-2026-10-08-001" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.wikipedia.org", "device": "iphone_15", "full_page": true, "block_cookie_banners": true, "format": "jpeg", "response": "json" }' ``` ## Headers | Header | Description | | --- | --- | | `Authorization` (required) | `Bearer` followed by your API key. The key is never accepted in the address. See [Your API key](https://docs.localscreenshot.com/auth). | | `Idempotency-Key` | Makes a retry safe: the same key returns the same screenshot and is charged once. 8 to 128 letters, digits, dashes, dots, colons or underscores. See [Retries and Idempotency-Key](https://docs.localscreenshot.com/retries). | ## What to capture | Parameter | Type | Description | | --- | --- | --- | | `url` (required) | string | The public http or https page to capture. | | `full_page` | boolean | Capture the whole scrollable page instead of the visible part. Default: `false`. | | `selector` | string | Capture only the first element matching this CSS selector. Up to 200 characters. | | `hide_selectors` | array | Hide every element matching these CSS selectors. In a URL, separate them with commas. Up to 20. | ## How it looks | Parameter | Type | Description | | --- | --- | --- | | `viewport` | string | Screen size as WIDTHxHEIGHT, for example 1440x900. Shortcut for viewport_width and viewport_height. | | `viewport_width` | integer | Width of the browser window, in pixels. 320 to 3,840. Default: `1440`. | | `viewport_height` | integer | Height of the browser window, in pixels. With full_page, the image is taller than this. 320 to 2,160. Default: `900`. | | `device` | string | Capture as this device (screen size, pixel density, mobile browser). One of `desktop`, `desktop_hd`, `laptop`, `ipad`, `ipad_pro`, `ipad_mini`, `iphone_15`, `iphone_15_pro_max`, `iphone_se`, `pixel_8`, `galaxy_s24`. | | `device_scale_factor` | number | Pixel density. 2 gives a sharper, larger image. 1 to 3. Default: `1`. | | `color_scheme` | string | Ask the page for its light or dark theme. One of `light`, `dark`. | | `language` | string | Browser language, for example fr or fr-FR. Default: `en-US`. | | `timezone` | string | Browser time zone, for example Europe/Paris. | | `country` | string | Load the page from this country (ISO 3166-1 alpha-2 code, see /v1/countries). The response reports the country actually observed. If it cannot be obtained, the request fails and is not charged. Costs 2 credits instead of 1. | > **Not open to every account yet.** Screenshots from a chosen country are being opened account by account. Until yours is, a request with `country` is refused with `option_unavailable` before anything is captured. ## Clean up | Parameter | Type | Description | | --- | --- | --- | | `block_cookie_banners` | boolean | Removes most cookie banners. A banner the filters do not know stays in the picture. Default: `false`. | | `block_ads` | boolean | Block ads. Default: `false`. | | `block_chat_widgets` | boolean | Hide chat bubbles. Default: `false`. | | `close_popups` | boolean | Close pop-ups and newsletter overlays. Default: `false`. | | `hide_sticky_elements` | boolean | Hide headers, bars and buttons that stay fixed on screen. Default: `false`. | | `disable_animations` | boolean | Freeze animations and transitions. Default: `false`. | ## When | Parameter | Type | Description | | --- | --- | --- | | `wait_for` | string | Wait until an element matching this CSS selector is visible. Up to 200 characters. | | `delay_ms` | integer | Extra wait before the capture, in milliseconds. 0 to 5,000. Default: `0`. | We wait for the page to load and for its network to calm down before the picture. Use `wait_for` when the part you want appears late, and `delay_ms` for an animation that needs a moment. ## What comes back | Parameter | Type | Description | | --- | --- | --- | | `format` | string | File format. pdf saves the whole page as a document; it cannot be combined with selector or max_width. One of `png`, `jpeg`, `webp`, `pdf`. Default: `png`. | | `quality` | integer | Quality for jpeg and webp. Ignored for png and pdf. 1 to 100. Default: `80`. | | `max_width` | integer | Scale the image down to this width. Useful to keep it small for a vision model. 320 to 3,840. | | `response` | string | image returns the picture itself. json returns its link, the page details and the attestation. One of `image`, `json`. Default: `image`. | | `async` | boolean | Return an id right away instead of waiting. Read the result at /v1/screenshots/{id}. Default: `false`. | > An option we do not know is refused with `invalid_request`, never ignored. `selector` and `full_page` cannot be used together. A `pdf` is always the whole page: it cannot be combined with `selector` or `max_width`. ## Response The image, with these headers. With `response=json`, a JSON document instead. If the screenshot is still running when the wait ends (after 50 seconds), or with `async=true`, you get `202` and a `Location` header to read it later: see [Run in the background](https://docs.localscreenshot.com/async). | Header | Description | | --- | --- | | `X-Screenshot-Id` | The id of the screenshot. | | `X-Credits-Charged` | Credits this screenshot used. | | `X-Credits-Remaining` | Credits you have left. | | `X-Page-Status` | What the page answered, for example 200 or 404. A page that answers 404 or 451 is still captured and charged. | | `X-Country-Observed` | Only with `country`: the country the page was really loaded from. | A `pdf` is sent as a file to download, not shown in the browser. **200, with response=json** ```json { "id": "01JABCDEF2G3H4J5K6M7N8P9QR", "status": "succeeded", "created_at": "2026-10-08T10:47:02Z", "finished_at": "2026-10-08T10:47:06Z", "image": { "url": "https://api.localscreenshot.com/v1/screenshots/01JABC.../image?expires=...&signature=...", "mime": "image/jpeg", "bytes": 412840, "width": 1179, "height": 8421, "pages": null, "sha256": "a5b9081b8430...", "truncated": false, "available_until": "2026-10-15T10:47:02Z" }, "page": { "status": 200, "final_url": "https://www.wikipedia.org/", "title": "Wikipedia", "redirects": [] }, "country": null, "attestation": null, "credits": { "charged": 1, "reserved": 1, "remaining": 49 }, "error": null, "links": { "self": "https://api.localscreenshot.com/v1/screenshots/01JABC..." } } ``` ## Limits of one screenshot - Only public `http` and `https` pages. A private or local address is refused with `url_not_allowed`. - A page has 35 seconds to load. After that the screenshot fails with `timeout`. - A page heavier than 10 MB to download fails with `page_too_heavy`. Video and audio are not loaded. - When a site shows a bot check instead of its page, the screenshot fails with `challenge_page`. We do not solve CAPTCHAs. Every code is on [Errors](https://docs.localscreenshot.com/errors). --- Source: https://docs.localscreenshot.com/read # Read a screenshot Every screenshot has an id. Use it to read the screenshot again, get a fresh link to its image, list what you took, or delete it. ## Read one `GET` `/v1/screenshots/{id}` Returns the screenshot as JSON: its status, its image, what the page answered, and the credits it used. The id is in the `X-Screenshot-Id` header of the answer that took it, and in the `id` field with `response=json`. **Read a screenshot** ```bash curl "https://api.localscreenshot.com/v1/screenshots/01JABCDEF2G3H4J5K6M7N8P9QR" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` | Field | Description | | --- | --- | | `status` | `queued`, `running`, `succeeded` or `failed`. | | `image` | The file: a link, its type, size and dimensions. `null` until the screenshot has succeeded. | | `page` | What the page answered: status, final address after redirects, title. | | `country` | Only when you asked for a country: the one requested and the one observed. | | `attestation` | Only when you asked for a country: the signed technical attestation. See [Countries and attestation](https://docs.localscreenshot.com/countries). | | `credits` | Credits charged, reserved and left. | | `error` | `null`, or the reason the screenshot failed. See [Errors](https://docs.localscreenshot.com/errors). | A screenshot you do not own, or one that was deleted, answers `not_found`. ## The link to the image `image.url` is a signed link. It opens without your API key, so you can hand it to a browser or to another tool. - The link works for 15 minutes. Read the screenshot again to get a fresh one. - The image itself is kept for 7 days. `image.available_until` says until when. After that the file is destroyed and only the record of the screenshot remains. - Download the image if you need it longer. **The image field** ```json { "url": "https://api.localscreenshot.com/v1/screenshots/01JABC.../image?expires=...&signature=...", "mime": "image/png", "bytes": 83672, "width": 1440, "height": 900, "pages": null, "sha256": "a5b9081b8430...", "truncated": false, "available_until": "2026-10-15T10:47:02Z" } ``` ## List your screenshots `GET` `/v1/screenshots` Returns your screenshots, newest first, by pages. | Parameter | Description | | --- | --- | | `limit` | How many to return, 1 to 100. Default: 20. | | `cursor` | The `next_cursor` of the previous page. | `next_cursor` is `null` on the last page. Deleted screenshots are not listed. **List, 50 at a time** ```bash curl "https://api.localscreenshot.com/v1/screenshots?limit=50" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` **200** ```json { "data": [ { "id": "01JABCDEF2G3H4J5K6M7N8P9QR", "status": "succeeded", "image": { "url": "...", "mime": "image/png" } } ], "next_cursor": "eyJjcmVhdGVkX2F0Ijoi..." } ``` ## Delete one `DELETE` `/v1/screenshots/{id}` Deletes a screenshot. It answers `204` with no body. - The image and its details are destroyed, and its links stop working. - A screenshot that is still running is cancelled and not charged. - Credits already used are not given back. - Deleting the same screenshot twice answers `204` both times. **Delete a screenshot** ```bash curl -X DELETE "https://api.localscreenshot.com/v1/screenshots/01JABCDEF2G3H4J5K6M7N8P9QR" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` --- Source: https://docs.localscreenshot.com/async # Run in the background By default a request waits for its screenshot. Add `async=true` to get an id right away and read the result later. Use it for long pages, for batches, or when your own request must answer fast. ## Start a screenshot Send the same request as usual, with `async=true`. The answer is `202` with the screenshot in its first state. **Start in the background** ```bash curl -X POST "https://api.localscreenshot.com/v1/screenshot" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.wikipedia.org", "full_page": true, "async": true }' ``` **202** ```http HTTP/1.1 202 Accepted Location: https://api.localscreenshot.com/v1/screenshots/01JABCDEF2G3H4J5K6M7N8P9QR Retry-After: 2 ``` **The body** ```json { "id": "01JABCDEF2G3H4J5K6M7N8P9QR", "status": "queued", "image": null, "page": null, "credits": { "charged": null, "reserved": 1, "remaining": 49 }, "error": null, "links": { "self": "https://api.localscreenshot.com/v1/screenshots/01JABC..." } } ``` ## Read the result Read the address given in `Location` until `status` is `succeeded` or `failed`. Wait the number of seconds in `Retry-After` between two reads. **Read it** ```bash curl "https://api.localscreenshot.com/v1/screenshots/01JABCDEF2G3H4J5K6M7N8P9QR" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` | Status | What it means | | --- | --- | | `queued` | Waiting for a free browser. | | `running` | The page is being loaded. | | `succeeded` | Done. `image` holds the link. | | `failed` | Done, without an image. `error` says why. | Reading a screenshot answers `200` whatever its status: look at `status` and `error`, not at the HTTP code. ## When a normal request also answers 202 A request without `async` waits up to 50 seconds. If the screenshot is not finished by then, the answer is the same `202`, with the same `Location`. The work is not lost: read it the same way. ## Credits - The credits are reserved when the screenshot starts, and charged when it succeeds. A failed screenshot is not charged. - `credits.charged` is `null` while the screenshot is running, then says what it cost. - Screenshots in the background count toward the number you can run at once. See [Credits and limits](https://docs.localscreenshot.com/credits). > **No webhooks yet.** We do not call your server when a screenshot is done. Read the screenshot until it is finished. --- Source: https://docs.localscreenshot.com/formats # File formats Choose the file with `format`. Every format costs the same. ## The four formats | Format | Good for | Notes | | --- | --- | --- | | `png` | Sharp text and interfaces. The default. | No loss. Larger files. | | `jpeg` | Photos and long pages. | Smaller files. Set `quality`. `jpg` is accepted too. | | `webp` | The smallest images. | Set `quality`. | | `pdf` | A document to keep, print or send. | Always the whole page. | **A smaller image** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org&format=webp&quality=70" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o shot.webp ``` ## Quality `quality` goes from 1 to 100 and applies to `jpeg` and `webp`. It is ignored for `png` and `pdf`. Without it, we use 80. ## Scale the image down `max_width` scales the image down to a width in pixels, keeping its proportions. It is useful to keep an image small for a vision model. The MCP server uses 1,568 pixels unless you say otherwise. ## PDF A `pdf` holds the whole page, on as many sheets as it needs. - It cannot be combined with `selector` or `max_width`. - `image.pages` gives the number of pages. `image.width` and `image.height` are `null`. - It is sent as a file to download, never shown inside a page. **Save a page as a PDF** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org&format=pdf" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o page.pdf ``` ## What you get back By default the answer is the file itself, with its type in `Content-Type`. With `response=json` you get a description of the file and a link to it. | Format | Content-Type | | --- | --- | | `png` | `image/png` | | `jpeg` | `image/jpeg` | | `webp` | `image/webp` | | `pdf` | `application/pdf` | **The image field, for a PDF** ```json { "url": "https://api.localscreenshot.com/v1/screenshots/01JABC.../image?expires=...&signature=...", "mime": "application/pdf", "bytes": 318422, "width": null, "height": null, "pages": 3, "sha256": "9c1d0f6ab2e4...", "truncated": false, "available_until": "2026-10-15T10:47:02Z" } ``` ## Very long pages - `image.truncated` is `true` when the page was longer than one file can hold: the end is missing. - When the picture would be too large to produce at all, the screenshot fails with `image_too_large`. Use a smaller viewport, or turn off `full_page`. - `image.sha256` is the fingerprint of the file. Use it to check a download. --- Source: https://docs.localscreenshot.com/devices # Devices and sizes A page does not look the same on a laptop and on a phone. Choose a device, or set the screen yourself. ## Pick a device `device` sets the screen size, the pixel density and, for a phone or a tablet, a mobile browser. One word instead of three numbers. - `desktop` - `desktop_hd` - `laptop` - `ipad` - `ipad_pro` - `ipad_mini` - `iphone_15` - `iphone_15_pro_max` - `iphone_se` - `pixel_8` - `galaxy_s24` **As a phone sees it** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org&device=iphone_15" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o phone.png ``` ## Or set the screen yourself Without `device`, the screen is 1,440 by 900 pixels. | Parameter | Type | Description | | --- | --- | --- | | `viewport` | string | Screen size as WIDTHxHEIGHT, for example 1440x900. Shortcut for viewport_width and viewport_height. | | `viewport_width` | integer | Width of the browser window, in pixels. 320 to 3,840. Default: `1440`. | | `viewport_height` | integer | Height of the browser window, in pixels. With full_page, the image is taller than this. 320 to 2,160. Default: `900`. | | `device` | string | Capture as this device (screen size, pixel density, mobile browser). One of `desktop`, `desktop_hd`, `laptop`, `ipad`, `ipad_pro`, `ipad_mini`, `iphone_15`, `iphone_15_pro_max`, `iphone_se`, `pixel_8`, `galaxy_s24`. | | `device_scale_factor` | number | Pixel density. 2 gives a sharper, larger image. 1 to 3. Default: `1`. | | `color_scheme` | string | Ask the page for its light or dark theme. One of `light`, `dark`. | | `language` | string | Browser language, for example fr or fr-FR. Default: `en-US`. | | `timezone` | string | Browser time zone, for example Europe/Paris. | | `country` | string | Load the page from this country (ISO 3166-1 alpha-2 code, see /v1/countries). The response reports the country actually observed. If it cannot be obtained, the request fails and is not charged. Costs 2 credits instead of 1. | `viewport=1280x720` is a shortcut for `viewport_width=1280` and `viewport_height=720`. **A custom screen, sharper** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org&viewport=1280x720&device_scale_factor=2" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o shot.png ``` ## The whole page, or one element - `full_page=true` captures the page from top to bottom. The image is as wide as the screen and as tall as the page. We scroll the page first, so images that load late are in the picture. - `selector` captures only the first element that matches a CSS selector, for example `#pricing` or `.chart`. If nothing matches, the screenshot fails with `selector_not_found`. - They cannot be used together. **One element only** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.wikipedia.org&selector=.central-featured" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -o element.png ``` ## Dark mode, language, time zone - `color_scheme=dark` asks the page for its dark theme. A page that has none stays as it is. - `language` sets the browser language, for example `fr` or `fr-FR`. A site that follows it answers in that language. - `timezone` sets the browser clock, for example `Europe/Paris`. These change what the browser says about itself. They do not change where the page is loaded from: for that, see [Countries and attestation](https://docs.localscreenshot.com/countries). --- Source: https://docs.localscreenshot.com/countries # Countries and attestation Add `country` to load a page the way people there get it. There are 227 countries. A screenshot from a chosen country uses 2 credits. > **Not open to every account yet.** Screenshots from a chosen country are being opened account by account. Until yours is, a request with `country` is refused with `option_unavailable` before anything is captured. Everything below describes how it works once it is open for you. **From Denmark** ```bash curl "https://api.localscreenshot.com/v1/screenshot?url=https://www.netflix.com&country=DK&response=json" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` ## The country you see is the one we observed We check where the page was really loaded from, with two independent location databases. The answer reports that country, never the one you asked for. - If we cannot confirm the country, you get the error `country_not_obtained`, and no screenshot from somewhere else. A failed screenshot is not charged. - With the image, the header `X-Country-Observed` carries the country. With `response=json`, it is in `country.observed`. - A country we do not have is refused with `country_not_available`. ## The list of countries `GET /v1/countries` returns the two-letter codes you can use. No key needed. The count is what we measured, on the date given in `measured_at`. **GET /v1/countries** ```json { "count": 227, "measured_at": "2026-10-08", "countries": [ { "code": "AD", "name": "Andorra" }, { "code": "AE", "name": "United Arab Emirates" } ] } ``` ## The attestation Every screenshot taken from a country comes with a signed record: where it was loaded from, when, what the page answered, and a fingerprint of the image. It is in the `attestation` field, and at `GET /v1/screenshots/{id}/attestation`. | Field | Description | | --- | --- | | `requested_country` | The country you asked for. | | `observed` | What we saw: country, kind of network, the network operator, a masked address, and what each location database said. | | `response` | What the page answered: status, final address, redirects, title. | | `screenshot_sha256` | The fingerprint of the image file. | | `strict_pass` | `true` only when the observed country matches and stayed the same during the capture. | | `signature` | An Ed25519 signature, with the id of the key that made it. | The address the page was loaded from is never given in full: the attestation keeps only its masked form. **The attestation** ```json { "schema": "attestation.v1", "id": "att_01JABCDEF2G3H4J5K6M7N8P9QR", "capture_id": "01JABCDEF2G3H4J5K6M7N8P9QR", "captured_at": "2026-10-08T10:47:06Z", "requested_country": "DK", "observed": { "country": "DK", "network_type": "residential_or_mobile_isp", "ip_prefix": "203.0.113.0/24", "ip_hash": "5f2c9a...", "asn": 64500, "asn_org": "Example Telecom", "geoip": [ { "db": "first", "version": "2026-10", "country": "DK" }, { "db": "second", "version": "2026-10", "country": "DK" } ], "geoip_agree": true, "exit_stable": true }, "response": { "status": 200, "final_url": "https://www.netflix.com/dk/", "redirects": [], "title": "Netflix" }, "screenshot_sha256": "a5b9081b8430...", "strict": true, "strict_pass": true, "signature": { "algorithm": "Ed25519", "key_id": "k20261008", "value": "MEUC..." } } ``` > **What it is, and is not.** A technical attestation of one capture, signed by us. It is not legal proof. ## Check the signature 1. Get our public keys at `GET /v1/attestation-keys` and pick the one whose `key_id` matches. 2. Remove the `signature` field from the attestation. 3. Write what is left as JSON with keys sorted at every level, with no escaping of slashes or accents. 4. Verify the base64 signature against that text with Ed25519. **GET /v1/attestation-keys** ```json { "keys": [ { "key_id": "k20261008", "algorithm": "Ed25519", "public_key": "base64..." } ] } ``` ## What a country changes, and what it does not - The page is loaded through a network connection in that country, so the site answers as it does for people there: prices, language, availability, legal notices. - It does not set the browser language or clock. Add `language` and `timezone` if the site follows those: see [Devices and sizes](https://docs.localscreenshot.com/devices). - We make no promise that a website will not notice the visit. If it shows a bot check instead of its page, the screenshot fails with `challenge_page`. --- Source: https://docs.localscreenshot.com/credits # Credits and limits You pay in credits, one screenshot at a time. A failed screenshot is not charged. ## What a screenshot costs | What you ask for | Credits | | --- | --- | | A screenshot | 1 | | A screenshot from a chosen country | 2 | Every other option is included: device, full page, format, cleaning up the page. - The credits are reserved when the screenshot starts and charged when it succeeds. - A page that answers an error of its own, such as 404 or 451, is still a screenshot: it is captured and charged. - Asking again with the same `Idempotency-Key` returns the same screenshot and is not charged twice. See [Retries and Idempotency-Key](https://docs.localscreenshot.com/retries). ## Where credits come from - **Signup credits.** 50 credits when you create your account. They do not expire. - **A monthly plan.** A number of credits each month. What is left at the end of the paid month does not carry over. - **Packs.** Bought once. They do not expire. The credits that expire first are used first. ## See what is left `GET` `/v1/credits` **Your credits** ```bash curl "https://api.localscreenshot.com/v1/credits" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" ``` **200** ```json { "credits": { "remaining": 49 }, "costs": { "base": 1, "surcharges": { "country": 1 } }, "limits": { "requests_per_minute": 10, "concurrent_screenshots": 2 } } ``` With the image, two headers say the same thing: `X-Credits-Charged` and `X-Credits-Remaining`. When you run out, a request fails with `insufficient_credits` and status 402. ## Limits Limits apply to the account. Several API keys share them. | Limit | On free credits | After a first payment | | --- | --- | --- | | Requests per minute | 10 | 120 | | Screenshots running at once | 2 | 20 | | Failed screenshots per day | 15 | 300 | - Over the number of requests per minute: `rate_limited`, with a `Retry-After` header. - Too many screenshots running at once: `concurrency_limit`. - Too many failed screenshots in a day: `failure_budget_exceeded`. ## Limits that protect websites A single website does not get more than 4 screenshots at once, nor more than 30 a minute, from all our users together. Past that, a request fails with `domain_busy`: try again a minute later. ## Limits of one screenshot | Limit | Value | | --- | --- | | Time for a page to load | 35 seconds | | Weight of a page | 10 MB | | How long a request waits before answering `202` | 50 seconds | | How long an image is kept | 7 days | | How long a link to an image works | 15 minutes | --- Source: https://docs.localscreenshot.com/errors # Errors Every error has a stable `code`, a plain message and a `retryable` flag. Retry only when it says yes, and wait for the `Retry-After` header when there is one. - A failed screenshot is not charged. - The number of failed screenshots per day is limited: 15 on free credits, 300 after a first payment. **402, refused** ```json { "error": { "code": "insufficient_credits", "message": "Not enough credits for this screenshot.", "retryable": false, "details": { "needed": 2, "available": 1 } } } ``` ## Refused before anything is captured Nothing was tried, nothing was charged. The body is `{"error": {...}}`. With `invalid_request`, `details` names the parameters to fix. | Code | HTTP | Retry | What it means | | --- | --- | --- | --- | | `invalid_request` | 422 | no | The request is not valid. Check the parameters listed in "details". | | `unauthorized` | 401 | no | Missing or invalid API key. Send it in the "Authorization: Bearer" header. | | `account_blocked` | 403 | no | Screenshots are paused for this account. Contact support. | | `insufficient_credits` | 402 | no | Not enough credits for this screenshot. | | `rate_limited` | 429 | yes | Too many requests. Wait a moment and try again. | | `concurrency_limit` | 429 | yes | Too many screenshots running at once for this account. Wait for one to finish. | | `failure_budget_exceeded` | 429 | no | Too many failed screenshots today for this account. Failed screenshots are free, so their number per day is limited. | | `spend_limit_reached` | 503 | yes | Screenshots are temporarily paused. Try again later. | | `url_not_allowed` | 422 | no | This URL cannot be captured. Only public http and https pages are allowed. | | `domain_blocked` | 403 | no | This website cannot be captured with this service. | | `domain_busy` | 429 | yes | This website is receiving too many screenshots right now. Try again in a minute. | | `country_not_available` | 422 | no | This country is not available. See GET /v1/countries. | | `service_paused` | 503 | yes | Screenshots are temporarily paused. Try again later. | | `idempotency_conflict` | 409 | no | This Idempotency-Key was already used with different parameters. | | `not_found` | 404 | no | Not found. | **422, a parameter we do not know** ```json { "error": { "code": "invalid_request", "message": "The request is not valid. Check the parameters listed in \"details\".", "retryable": false, "details": { "unknown_parameters": ["fullpage"], "allowed_parameters": ["url", "country", "viewport", "..."] } } } ``` ## The screenshot was tried and failed The page was opened, or we tried to. Most of the time the body is the screenshot itself, with `status` set to `failed` and its `error` filled in, so you keep its id. `option_unavailable` can also be answered before anything is tried, for example when you ask for a country that is not open to your account. The body is then `{"error": {...}}`, as in the first table. | Code | HTTP | Retry | What it means | | --- | --- | --- | --- | | `country_not_obtained` | 502 | yes | The page could not be loaded from the requested country, so no screenshot was taken. You were not charged. Try again. | | `challenge_page` | 502 | no | The website showed a bot check instead of the page. We do not bypass bot checks. | | `timeout` | 504 | yes | The page took too long to load. | | `page_too_heavy` | 502 | no | The page is too heavy to capture (over the size limit). | | `too_many_redirects` | 502 | no | The page redirected too many times. | | `selector_not_found` | 422 | no | No element matches "selector" on this page. | | `wait_for_timeout` | 422 | no | The "wait_for" element never appeared. | | `navigation_failed` | 502 | no | The page could not be opened. Check the address. | | `image_too_large` | 502 | no | The screenshot would be too large. Use a smaller viewport or turn off full_page. | | `option_unavailable` | 503 | no | One of the requested options is not available right now. | | `image_withheld` | 502 | no | The page displays the network address used to load it, so the screenshot was not kept. You were not charged. | | `engine_busy` | 503 | yes | All browsers are busy. Try again in a few seconds. | | `canceled` | 409 | no | The screenshot was cancelled before it finished. | | `internal_error` | 500 | yes | Something went wrong on our side. You were not charged. | **502, tried and failed** ```json { "id": "01JABCDEF2G3H4J5K6M7N8P9QR", "status": "failed", "image": null, "credits": { "charged": 0, "reserved": 1, "remaining": 49 }, "error": { "code": "challenge_page", "message": "The website showed a bot check instead of the page. We do not bypass bot checks.", "retryable": false } } ``` ## What to do with an error 1. Read `error.code`, not the message: the code is stable, the wording may change. 2. If `retryable` is `false`, change the request before sending it again. 3. If `retryable` is `true`, wait for `Retry-After` when it is there, then try again with the same `Idempotency-Key`. 4. Stop after a few tries. See [Retries and Idempotency-Key](https://docs.localscreenshot.com/retries). **429, wait and retry** ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 ``` ## Through the MCP server Your AI gets the same message as a plain sentence, followed by the code, for example `(code: challenge_page, retryable: no)`. It can tell you what happened without reading JSON. --- Source: https://docs.localscreenshot.com/retries # Retries and Idempotency-Key A request can fail on the way: a timeout, a dropped connection. Send an `Idempotency-Key` with it, and trying again is safe: you get the same screenshot, charged once. ## Send an Idempotency-Key Choose a value that is unique for each screenshot you want, and send it in the `Idempotency-Key` header. **A request you can repeat** ```bash curl -X POST "https://api.localscreenshot.com/v1/screenshot" \ -H "Authorization: Bearer $LOCALSCREENSHOT_API_KEY" \ -H "Idempotency-Key: order-2026-10-08-001" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.wikipedia.org", "full_page": true }' ``` - 8 to 128 characters: letters, digits, dashes, underscores, dots and colons. - A random id works well, or an id from your own system, such as an order number. - Keys belong to your account. Another account using the same value gets its own screenshot. ## What the same key does | You send | You get | | --- | --- | | A key for the first time | A new screenshot. | | The same key, the same options | The same screenshot again. Nothing new is charged. | | The same key, different options | The error `idempotency_conflict`, with status 409. | | The same key while the first request is still running | The same screenshot, in the state it has reached. | `response` and `async` describe the answer, not the screenshot: you can change them between two tries with the same key. A key is remembered as long as its screenshot is kept, which is 7 days. Deleting a screenshot frees its key at once. ## When to retry Look at `retryable` in the error. See [Errors](https://docs.localscreenshot.com/errors) for every code. - `retryable` is `true`: try again as is. Wait for the `Retry-After` header when there is one. - `retryable` is `false`: the same request would fail the same way. Change it first. - No answer at all (your own timeout, a lost connection): try again with the same `Idempotency-Key`. If the first request did go through, you get its screenshot. **429, wait and retry** ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 ``` ## A good retry loop 1. Send the request with an `Idempotency-Key`. 2. On an error with `retryable: true`, wait. Use `Retry-After` if it is there, otherwise 2 seconds, then 4, then 8. 3. Send the same request with the same key. 4. Stop after 3 or 4 tries and report the error. The number of failed screenshots per day is limited: 15 on free credits, 300 after a first payment. A loop that never stops would use it up, and you would get `failure_budget_exceeded` until the next day. ## A screenshot that takes a long time A request waits up to 50 seconds. If the screenshot is still running then, you get `202` and its id: this is not an error, and there is nothing to send again. Read the screenshot until it is done. See [Run in the background](https://docs.localscreenshot.com/async).