Skip to content
apiscreenshotGitHub

Open-source screenshot API

Full-page screenshots,
minus the cookie banner.

Deploy it to your own Cloudflare account in one click, or run it on any server with Docker. Banners, pop-ups and chat widgets are gone before the shot is taken.

After: the same page with the pop-up and the cookie box gone
Before: the Allbirds home page, dimmed behind a country pop-up, with a cookie consent box over the bottom
GET /screenshot?url=allbirds.com200 · x-consent-manager: Shopify · x-removed-elements: 2

What it removes

The page as it looks once everything is closed.

The browser loads the page, rejects the cookie banner, closes pop-ups, hides any chat widget and scrolls to the bottom so every image loads. Then it takes the shot and tells you what it removed.

The Booking.com home page as it loads: a sign-in pop-up over a dimmed page, and cookie settings across the bottom
BeforeWhat the browser loaded. Outlined in amber: what the API removed.

The response

Request
GET /screenshot?url=https://www.booking.com&full_page=true
status:
200 · image/png
x-final-url:
https://www.booking.com/
x-target-status:
200
x-consent-manager:
OneTrust
x-removed-elements:
2
x-page-height:
5465
The full Booking.com page the API returned, with nothing covering it
AfterThe full page the API sent back. Open the whole page

Examples

Pick a site. See what came back.

Real requests to the API on Cloudflare. The page as it first loaded, next to the shot that came back.

Le Monde's front page with no consent box: menu, latest news and the lead story
Le Monde's front page greyed out behind a consent box asking to accept data use or subscribe
A consent wall that greys out the whole front page. The page under it is left as it was.

The request

curl -G "$API_URL/screenshot" \ -d url=https://www.lemonde.fr \ -d full_page=true \ -H "Authorization: Bearer $API_KEY" \ -o lemonde.png

The response

status:
200 · image/png
x-removed-elements:
2
x-page-height:
13296
x-truncated:
true

Took 12.8 seconds, on Cloudflare.

Taken on 15 November 2026. Each “before” is the same request without full_page and with block_cookie_banners, block_popups, block_chats, block_ads and block_trackers set to false. Full-page shots are cut to their first screen here.

The API

One request. An image back, or the reason why not.

Send the URL and options as query parameters or a JSON body. When a site shows a captcha, an error or an empty page, you get an error that says so, never a picture of it.

Take a full-page shot
curl -G "$API_URL/screenshot" \
  -d url=https://example.com \
  -d full_page=true \
  -H "Authorization: Bearer $API_KEY" \
  -o example.png
HTTP 422: the site blocked the browser
{  "error": "blocked",  "reason": "captcha",  "message": "Captcha on the page."}

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.

Every option, header and error code is in the README.

Run it

On your Cloudflare account, or on your own server.

The same API and options either way.

Cloudflare Workers

The quickest start. It uses Cloudflare's Browser Rendering, which the free plan includes.

  1. 1Click the button. Cloudflare copies the repository to your GitHub account and deploys it.
  2. 2When it asks for API_KEY, paste a long random string, such as the output of openssl rand -hex 32. Keep it: every request needs it.
  3. 3Call /screenshot on your workers.dev address, https://api-screenshot.<you>.workers.dev.

Docker

For any server, or your own machine. Use it for sites that block Cloudflare's browser: on your server, the browser isn't labelled as a bot.

export API_KEY=$(openssl rand -hex 32)
docker run -d -p 3000:3000 \ -e API_KEY="$API_KEY" ghcr.io/matbrons/api-screenshot
curl "localhost:3000/screenshot?url=example.com" \ -H "Authorization: Bearer $API_KEY" -o shot.png

What it handles

What gets in the way of a clean shot

  • Cookie banners

    Clicks reject where it can, using DuckDuckGo's autoconsent rules for hundreds of consent tools. Leftovers are hidden.

  • Pop-ups

    Newsletter and sign-up modals are closed, and a page locked behind one can scroll again.

  • Chat widgets

    Intercom, HubSpot, Drift, Crisp, Zendesk and other launchers are hidden from the shot.

  • Lazy images

    The page is scrolled from top to bottom first, so images that load late are in the picture.

  • Full page

    The whole page, including sites that scroll inside their own container. Cut at 10,000 px by default (full_page_max_height) and 16,384 device pixels at most. The x-truncated header says when.

  • Blocked or blank pages

    A captcha, bot wall, error page or empty render comes back as an error with a reason. Never as a picture of it.

  • SSRF guard

    Every URL, redirect and request the page makes is checked. Private, local and cloud metadata addresses are refused.

  • API key

    Every request has to carry your API_KEY. Until you set one, the API takes no shots. Size and time limits are yours to set.

Limits

What it doesn't do well

Worth knowing before you build on it.

  1. 01

    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.

    Cloudflare's docs
  2. 02

    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.

    Cloudflare's pricing
  3. 03

    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.

  4. 04

    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

Questions

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.

Your own screenshot API, on your own account.

Free under the MIT licence. Read the code, change it, run it where you like.