# apiscreenshot
> A free, MIT-licensed screenshot API you run yourself: one click into your own Cloudflare account (Workers and Browser Rendering), or a Docker image for any server. It takes full-page shots with cookie banners, pop-ups and chat widgets removed, and reports blocked, captcha, error and blank pages as errors instead of returning them as images.
## Run it
- [Deploy to Cloudflare](https://deploy.workers.cloudflare.com/?url=https://github.com/matbrons/api-screenshot): copies the repository to your GitHub account and deploys it as a Worker. When it asks for API_KEY, paste a long random string, such as the output of `openssl rand -hex 32`.
- Docker: `export API_KEY=$(openssl rand -hex 32)`, then `docker run -d -p 3000:3000 -e API_KEY="$API_KEY" ghcr.io/matbrons/api-screenshot`, then `curl -H "Authorization: Bearer $API_KEY" "http://localhost:3000/screenshot?url=example.com" -o shot.png`.
- Without Docker (Node 22.18 or newer, Chrome or Chromium installed, `CHROME_PATH` if it isn't found): `export API_KEY=$(openssl rand -hex 32)`, then `npm install` and `HOST=127.0.0.1 npm start`.
- [Source code](https://github.com/matbrons/api-screenshot) and [README](https://github.com/matbrons/api-screenshot#readme) with every option, header and error code.
## API
`GET /screenshot?url=...` (or POST with a JSON body). Success returns the image or PDF, or JSON with `response_type=json`. Errors are JSON: `{"error", "message", "reason"}`; `blocked`, `blank` and `error_page` come back as HTTP 422. Until API_KEY is set (16 characters or more), every shot is refused with `not_configured`. Once it is, each request must carry it as a Bearer token or an X-API-Key header, never in the URL. For a shot in an
tag or a link, use a signed URL made with SIGNING_SECRET (README, "Signed URLs"). ALLOW_PUBLIC_ACCESS=true opens the API to anyone instead.
AI agents can use it over MCP at `/mcp` (Streamable HTTP, the same key), with two tools: `screenshot` returns the image and `page_details` returns the details as JSON. For Claude Code: `claude mcp add --transport http screenshot https:///mcp --header "Authorization: Bearer "`. `/openapi.json` describes the HTTP API in OpenAPI 3.1 and needs no key.
Main options:
- `url` (default: required): The page to capture. https:// is added when you leave it out.
- `full_page` (default: false): Capture the whole page, not just the first screen.
- `viewport_width` (default: 1280): Browser width in pixels. viewport_height is 800.
- `viewport_device` (default: none): A phone, tablet or laptop's size and user agent, such as iphone_15 or ipad_landscape.
- `format` (default: png): png, jpeg, webp or pdf. image_quality sets jpeg and webp quality.
- `response_type` (default: by_format): json for the title, description, Open Graph tags and icon, with the image in base64.
- `block_cookie_banners` (default: true): Reject or close consent banners before the shot.
- `block_popups` (default: true): Close newsletter and sign-up modals.
- `block_chats` (default: true): Hide chat launchers and similar corner widgets.
- `block_ads` (default: true): Block ad networks and hide ad boxes, using EasyList.
- `block_trackers` (default: true): Block tracking scripts and pixels, using EasyPrivacy.
- `hide_selectors` (default: none): Your own CSS selectors to hide, comma-separated.
- `selector` (default: none): Capture one element instead of the page.
- `wait_for_selector` (default: none): Wait until this element shows up.
- `click` (default: none): Click these elements first, such as a tab or a "Show more" button.
- `scripts` (default: none): Your own JavaScript, run in the page before the shot. styles adds CSS.
- `block_requests` (default: none): Hosts or URL patterns not to load. block_resources skips kinds, such as font.
- `headers` (default: none): Extra headers for the page's own site. cookies sets cookies first.
- `html` (default: none): Your own markup to render instead of a URL, in a POST body.
- `dark_mode` (default: false): Ask the page for its dark theme.
## Limits
- Some sites block Cloudflare's browser: Cloudflare marks every request from its browser as a bot, so sites behind bot protection show a challenge. You get a blocked error back. For those sites, run the Docker image on your own server.
- The free plan is small: On the free Workers plan you get 10 browser-minutes a day, 3 browsers at a time and 1 new browser every 20 seconds. The $5 Workers Paid plan includes 10 browser-hours a month, then $0.09 an hour.
- Not every banner goes: Consent tools change all the time and some sites build their own. When one slips through, pass its selector in hide_selectors, and open an issue so the rules get better.
- No proxies of its own: Pages load from wherever the API runs. Cloudflare's browser can't use a proxy. On Docker you bring your own: PROXY_URL for one, or PROXY_URLS for a list it takes in turn, retrying through the next.
## FAQ
### Is it really free?
Yes. The code is MIT-licensed and there is no paid version. You pay your own host: nothing on Cloudflare's free plan within its limits, or whatever your server costs.
### Why not use a hosted screenshot API?
Hosted APIs that remove cookie banners cost about $17 to $79 a month for a few thousand shots. They are a good choice if you need proxies, many regions or a support contract. If you want clean shots of ordinary sites on your own account, this does that for the price of your Cloudflare plan.
### Do I need a paid Cloudflare plan?
No. The deploy button works on a free account. The free plan's 10 browser-minutes a day is enough for testing and light use; at 5 seconds a shot that is about 120 shots a day.
### Does it accept cookies on my behalf?
It clicks reject when the banner offers it. When a banner only has an accept button, it accepts so the page unlocks. The browser is thrown away after each shot, so nothing is kept.
### What happens when a site blocks it?
You get HTTP 422 with a JSON body such as {"error": "blocked", "reason": "captcha"}. Blank pages and error pages are reported the same way, so a broken shot never ends up in your app looking like a real one.
### Can people use my deployment without asking?
No. It takes no shots until you set API_KEY, and then every request needs it, as a Bearer token or an X-API-Key header. For an image tag or a link, use a signed URL instead. Only ALLOW_PUBLIC_ACCESS=true opens it to anyone.
### Does it send data anywhere?
There is no telemetry. Besides the pages you ask for, it fetches the public filter lists for ads, trackers and cookie banners from EasyList: the Worker when it is built, the Docker server at start and once a day. On Docker, FETCH_BLOCKLISTS=false turns that off.
### Will requests written for other screenshot APIs work?
Often with small changes. The option names follow the ones hosted APIs use, such as url, full_page, viewport_width and block_cookie_banners. Unknown options are refused with a clear error, so a typo never fails quietly.
### Can an AI agent use it?
Yes. It is also an MCP server at /mcp, with the same API key, so Claude, Cursor and other MCP clients can take a screenshot and look at it. A captcha or bot wall comes back as an error the agent can read, not as an image of the wall. The README has the setup for each client.
## Blog
- [How many screenshots fit in Cloudflare's free plan](https://apiscreenshot.com/blog/cloudflare-free-plan-screenshots): Cloudflare's free Workers plan gives you 10 browser-minutes a day. Here is what that means in screenshots, and when the $5 plan is worth it.