Take a website screenshot in eight programming languages
One small task, written eight times: save a full-page PNG of https://example.com to a file. Every version is made of the same four parts. Point at a part, or press its number, and it lights up in every language at once.
The anatomy of a screenshot API request
A screenshot API request is one HTTP GET: an address to capture and a few options in the query string, an access key in a header, and an image in the response body. All eight versions above build exactly this request, so the numbers in the code point here.
GET https://curlshot.com/api/v1/screenshot
?url=https%3A%2F%2Fexample.com
&full_page=true
&format=png
X-Access-Key: YOUR_ACCESS_KEY
200 OK
Content-Type: image/png
(the bytes of example.png)The address
The
urlparameter: a full address that starts withhttp://orhttps://, up to 4,096 characters. It must be percent-encoded, which every snippet leaves to its HTTP client.The options
full_page=truecaptures the whole page instead of the first screen (the default isfalse).format=pngis the default and is written out so it is easy to change tojpeg,webporpdf.The access key
The
X-Access-Keyheader. A header keeps the key out of the address, so it does not end up in logs and browser history.The file
The response body is the PNG itself, with the content type
image/png. Write the bytes to a file unchanged, in binary mode.
The key can travel three ways: the X-Access-Key header used here, the standard Authorization: Bearer header, or an access_key query parameter for quick tests in a terminal. A failed request answers with a status of 400 or higher and a short JSON message, never a broken image. More in authentication and the options reference.
How do you take a screenshot with a headless browser?
A headless browser takes a screenshot in four calls: launch the browser, open a page, go to the address, call page.screenshot. Playwright publishes official libraries for Node.js, Python, Java and .NET[1][2][3][4], and Puppeteer is a Node.js library with the same shape[5]. The same numbers mark the same parts. Part 3 is missing: there is no key, and in its place you install, launch and close a browser.
The same task with your own browser
Track A: a library and a browser
Node.js, Playwright6 lines
screenshot.mjs import { chromium } from 'playwright' const browser = await chromium.launch() const page = await browser.newPage() await page.goto('https://example.com') await page.screenshot({ path: 'example.png', fullPage: true }) await browser.close()Run: npm install playwright, npx playwright install chromium, then node screenshot.mjs. The option is
fullPage.[1]Run and syntax-checked
Node.js, Puppeteer6 lines
screenshot.mjs import puppeteer from 'puppeteer' const browser = await puppeteer.launch() const page = await browser.newPage() await page.goto('https://example.com') await page.screenshot({ path: 'example.png', fullPage: true }) await browser.close()Run: npm install puppeteer (it downloads a browser), then node screenshot.mjs. The option is
fullPage, and it isfalseunless you set it.[5][6]Run and syntax-checked
Python, Playwright7 lines
screenshot.py from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("https://example.com") page.screenshot(path="example.png", full_page=True) browser.close()Run: pip install playwright, playwright install chromium, then python screenshot.py. The option is
full_page. Playwright for Python also has an async API with the same calls.[2]Run and syntax-checked
Java, Playwright14 lines
Screenshot.java import com.microsoft.playwright.*; import java.nio.file.Paths; public class Screenshot { public static void main(String[] args) { try (Playwright playwright = Playwright.create()) { Browser browser = playwright.chromium().launch(); Page page = browser.newPage(); page.navigate("https://example.com"); page.screenshot(new Page.ScreenshotOptions() .setPath(Paths.get("example.png")) .setFullPage(true)); } } }Run: Add the com.microsoft.playwright dependency to a Maven or Gradle project and run the class. The option is
setFullPage, onPage.ScreenshotOptions.[3]Checked against the documentation only
C#, Playwright6 lines
Program.cs using Microsoft.Playwright; using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.Chromium.LaunchAsync(); var page = await browser.NewPageAsync(); await page.GotoAsync("https://example.com"); await page.ScreenshotAsync(new() { Path = "example.png", FullPage = true });Run: Add the Microsoft.Playwright package to a console project, install its browsers as the Playwright for .NET documentation describes, then dotnet run. The option is
FullPage.[4]Checked against the documentation only
What exists for PHP, Go and Ruby?
Neither Playwright nor Puppeteer publishes a library for PHP, Go or Ruby. These projects exist. They are listed in the order of the language rail and are not ranked.
PHP
php-webdriver
A community project that describes itself as the "PHP language binding for Selenium WebDriver". It drives a browser through a WebDriver server that you run next to your code.[9]
Go
chromedp
A Go package that "drives browsers that speak the Chrome DevTools Protocol" and "needs no external driver". Its examples repository includes a full-page screenshot.[7]
Ruby
selenium-webdriver
The Ruby binding of the Selenium project, which documents it next to its Java, Python, C# and JavaScript bindings.
driver.save_screenshot('./image.png')saves the current browsing context; the Selenium page on screenshots does not describe a full-page option.[8]
Eight languages, two tracks: the matrix
The HTTP request takes between 6 and 31 lines depending on the language, and the headless-browser version between 6 and 14 where an official library exists. Lines are the non-blank lines of the snippets on this page, counted by the code that builds the page.
| Language | Official headless-browser library | HTTP client used here | Lines, track A | Lines, track B | As written here |
|---|---|---|---|---|---|
| curl | Official headless-browser libraryNone. curl sends requests; it does not drive a browser | HTTP client used herecurlthe tool itself | Lines, track Anone | Lines, track B6 | As written herea command |
| Node.js | Official headless-browser libraryPlaywright, Puppeteer | HTTP client used herefetchstandard library | Lines, track A6 (Playwright), 6 (Puppeteer) | Lines, track B10 | As written hereasync |
| Python | Official headless-browser libraryPlaywright | HTTP client used hererequestsone package | Lines, track A7 (Playwright) | Lines, track B12 | As written heresync |
| PHP | Official headless-browser libraryNone from Playwright or Puppeteer. Community: php-webdriver | HTTP client used herecurl extensionstandard library | Lines, track Anone | Lines, track B12 | As written heresync |
| Go | Official headless-browser libraryNone from Playwright or Puppeteer. Community: chromedp | HTTP client used herenet/httpstandard library | Lines, track Anone | Lines, track B31 | As written heresync |
| Ruby | Official headless-browser libraryNone from Playwright or Puppeteer. Selenium has a Ruby binding | HTTP client used herenet/httpstandard library | Lines, track Anone | Lines, track B13 | As written heresync |
| Java | Official headless-browser libraryPlaywright | HTTP client used herejava.net.http.HttpClientstandard library | Lines, track A14 (Playwright) | Lines, track B18 | As written heresync |
| C# | Official headless-browser libraryPlaywright | HTTP client used hereHttpClientstandard library | Lines, track A6 (Playwright) | Lines, track B7 | As written hereasync |
How the snippets on this page were checked
Run and syntax-checked: 8 of 13
curl (curl); Node.js (fetch); Python (requests); Go (net/http); Ruby (net/http); Node.js, Playwright; Node.js, Puppeteer; Python, Playwright.
Run on 11 October 2026. The track B versions ran against a local test server that checks the query string and the header and answers with a PNG; they were not run against the live API. The track A versions captured https://example.com.
Checked against the documentation only: 5 of 13
PHP (curl extension); Java (java.net.http.HttpClient); C# (HttpClient); Java, Playwright; C#, Playwright.
No toolchain for PHP, Java or .NET was on the machine this article was written on. These versions use only calls that appear in the official documentation cited next to them, and were not executed.
What should every version add next?
The shortest version of a screenshot request is not the one to ship. These five additions turn any of the eight into code that survives a bad day, each shown once, in one language. The same idea carries to the other seven.
1Check the HTTP status before you save
A failed request answers with a status of 400 or higher and a short JSON message, not an image. Seven of the eight versions above would save that JSON as
example.png. Save the body only when the status is 200 and the content type starts withimage/.Node.js if (!response.ok) { const error = await response.json() throw new Error(`${response.status} ${error.error_code}: ${error.error_message}`) }2Set a timeout longer than the render
A render can take as long as the
timeoutoption allows: 30 seconds by default and 90 at most. Give your own HTTP client more than that, for example 120 seconds, or a slow page will look like a network failure.Ruby response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 120) do |http| http.request(request) end3Retry 429 and 5xx with a pause
Status 429 means you are sending too fast or too many renders are running. Statuses 500, 502, 503 and 504 are temporary. On 429 and 503 the
Retry-Afterheader says how many seconds to wait; otherwise wait 1, 2, then 4 seconds. Do not retry 400, 401, 402 or 403: the same request fails the same way.Python for attempt in range(4): response = requests.get(api, params=params, headers=headers, timeout=120) if response.status_code not in (429, 500, 502, 503, 504): break time.sleep(int(response.headers.get("Retry-After") or 2 ** attempt))4Keep the key in an environment variable
A key typed into a file ends up in version control and in every copy of the code. Read it from the environment instead, and set the variable where the code runs.
curl export SCREENSHOT_ACCESS_KEY="YOUR_ACCESS_KEY" # once, outside the code curl -G "$API/screenshot" \ --data-urlencode "url=https://example.com" \ -H "X-Access-Key: $SCREENSHOT_ACCESS_KEY" \ --output example.png5Stream large responses to disk
A full-page PNG of a long page or a video can be many megabytes. Copy the body to the file as it arrives instead of holding all of it in memory first.
Go file, err := os.Create("example.png") if err != nil { log.Fatal(err) } defer file.Close() if _, err := io.Copy(file, resp.Body); err != nil { log.Fatal(err) }
Complete, runnable versions with all of this in place are in the code examples for curl, Node.js, Python, PHP, Go and Ruby, and every error code is listed in the errors reference.
Headless browser or screenshot API: which track when?
Drive a browser yourself when the page is private or the browser is already there; send an HTTP request when installing and running a browser is the hard part. Both are sound choices, and plenty of teams use both.
Drive a browser yourself when
- The page is on
localhostor a private network. A hosted API cannot reach those addresses. - You already run a browser for tests, so the screenshot is one more line in code you have.
- You need to script many steps before the capture: log in, fill a form, click through a flow.
- Your language has an official library: Node.js, Python, Java or .NET with Playwright, Node.js with Puppeteer.
- Nothing may leave your machines, including the address you capture.
Use a screenshot API when
- Your language has no official headless-browser library, such as PHP, Go or Ruby, or you only have a shell.
- You cannot install or update a browser where the code runs: a small container, a serverless function, shared hosting.
- You want the same result from every language and every machine, because the browser is not on any of them.
- You need options that take work to build yourself, such as hiding cookie banners and ads or emulating a device.
- The pages are public and the volume fits a plan with a monthly quota.
The longer comparison, with what it takes to run browsers in production, is in screenshot API vs headless browser. What the API side offers is on the screenshot API page.
Glossary: the words in the code
- HTTP client
- The part of a language or a tool that sends HTTP requests and reads the answers:
fetchin Node.js,net/httpin Go and Ruby,HttpClientin Java and C#, and curl in a terminal. - Query string
- The part of an address after the
?, a list ofname=valuepairs joined with&. The options of a screenshot request travel there. - Percent-encoding
- Writing a character that has a meaning in an address as
%and two hexadecimal digits, sohttps://example.cominside a query string becomeshttps%3A%2F%2Fexample.com. - Header
- A named line that travels with a request or a response, apart from the address and the body, such as
X-Access-Keyon the way in andContent-Typeon the way back. - Bearer token
- A key sent in the standard
Authorization: Bearer <token>header. Whoever holds ("bears") the token is let in, so it must stay secret. - Binary response
- A response whose body is raw bytes, such as a PNG, and not text. It must be written to a file unchanged: no text decoding, no line-ending conversion.
- Headless browser
- A real browser that runs without a window, driven by code. It loads and draws pages exactly as a visible browser does, which is why it can take screenshots.
- SDK
- A software development kit: a library that wraps an API in functions for one language. None is needed for any snippet on this page; an HTTP client is enough.
Questions people ask
What is the shortest way to take a screenshot of a website from code?
With a headless browser in Node.js or Python it is about six lines: launch a browser, open a page, go to the address, call page.screenshot with a file path, close the browser. With a screenshot API such as CurlShot it is one curl command: the address and the options in the query string, the access key in a header, and --output to save the file.
How do I take a full-page screenshot in Python?
With Playwright for Python, call page.screenshot(path="example.png", full_page=True) after page.goto. With a screenshot API, send a GET request with the requests package and the parameter full_page set to true, then write response.content to a file opened in "wb" mode.
Can I take a website screenshot in PHP, Go or Ruby without a headless browser library?
Yes. Playwright and Puppeteer publish no official library for PHP, Go or Ruby, but each of those languages has an HTTP client: the curl extension in PHP and net/http in Go and Ruby. One GET request to a screenshot API such as CurlShot returns the image, and the code writes the bytes to a file.
Do I need an SDK to use a screenshot API?
No. A screenshot API that answers a GET request with the image needs only an HTTP client, which every language on this page has in its standard library or one package away. The eight versions on this page use curl, fetch, requests, the PHP curl extension, net/http in Go and Ruby, java.net.http.HttpClient and .NET HttpClient.
Why is my saved screenshot file broken or only a few bytes long?
The request most likely failed and the code saved the error message as if it were an image. A failed request has a status of 400 or higher and a JSON body. Check the status before writing the file, and open the file in binary mode so the bytes are not changed on the way to disk.
Which errors from a screenshot API should my code retry?
With CurlShot, retry status 429 after the number of seconds in the Retry-After header, and retry 500, 502, 503 and 504 after a short pause that doubles each time. Do not retry 400, 401, 402 or 403: the request, the key or the quota has to change first.
Is it better to pass the API key in the URL or in a header?
A header is better for code that runs on a server, because addresses are written to logs and histories and a header is not part of the address. CurlShot accepts the key as the X-Access-Key header, as Authorization: Bearer, or as the access_key query parameter for quick tests in a terminal.
Sources
- [1]Playwright, Screenshots (Node.js). Checked .
- [2]Playwright, Screenshots (Python). Checked .
- [3]Playwright, Screenshots (Java). Checked .
- [4]Playwright, Screenshots (.NET). Checked .
- [5]Puppeteer, Screenshots guide. Checked .
- [6]Puppeteer, ScreenshotOptions interface. Checked .
- [7]chromedp on GitHub, chromedp README. Checked .
- [8]Selenium, Working with windows and tabs: TakeScreenshot. Checked .
- [9]php-webdriver on GitHub, php-webdriver README. Checked .
- [10]Oracle, HttpResponse.BodyHandlers (Java SE 21). Checked .
- [11]Oracle, HttpRequest (Java SE 21). Checked .
- [12]Oracle, URLEncoder (Java SE 21). Checked .
- [13]Microsoft Learn, HttpClient.GetByteArrayAsync method. Checked .
Read next
- CalculatorScreenshot API vs headless browser: the real costA cost calculator with every assumption editable, the hidden work behind headless Chrome, and an honest checklist for build or buy.
- How-toFull-page screenshot on every device and browserExact keys and menu names to screenshot a whole web page in Chrome, Firefox, Edge, Safari, iPhone, iPad and Android, checked against each vendor.
- PlaybookHow to screenshot a list of URLs in bulkScreenshot hundreds or thousands of URLs: a queue simulator that says how long it takes, a short Node.js script, and what fails at scale.