Skip to content
Rosetta stoneBy 9 min read

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.

Screenshot API request in curl, Node.js, Python, PHP, Go, Ruby, Java and C#

Track B: one HTTP request

  1. curl6 lines

    Terminal
    curl -G "https://curlshot.com/api/v1/screenshot" \
      --data-urlencode "url=https://example.com" \
      -d "full_page=true" \
      -d "format=png" \
      -H "X-Access-Key: YOUR_ACCESS_KEY" \
      --output example.png

    Run: Paste it into a terminal. -G sends the -d lines as a query string and --data-urlencode encodes the address. On an error, curl still writes the answer to the file unless you add --fail.

    Run and syntax-checked

  2. Python, requests12 lines

    screenshot.py
    import requests
    
    response = requests.get(
        "https://curlshot.com/api/v1/screenshot",
        params={
            "url": "https://example.com",
            "full_page": "true",
            "format": "png",
        },
        headers={"X-Access-Key": "YOUR_ACCESS_KEY"},
    )
    with open("example.png", "wb") as f:
        f.write(response.content)

    Run: pip install requests, then python screenshot.py. requests is one package to install. The standard library can do the same with urllib.request, in a few more lines.

    Run and syntax-checked

Each version sends the same request to CurlShot and writes the answer to example.png. Swap YOUR_ACCESS_KEY for a key from a free account. These are the shortest versions that work, and most of them do not check the answer yet: see what to add next.

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 request every version sends, and the answer, written out as HTTP. The address is percent-encoded: each language does that for you.
  1. The address

    The url parameter: a full address that starts with http:// or https://, up to 4,096 characters. It must be percent-encoded, which every snippet leaves to its HTTP client.

  2. The options

    full_page=true captures the whole page instead of the first screen (the default is false). format=png is the default and is written out so it is easy to change to jpeg, webp or pdf.

  3. The access key

    The X-Access-Key header. A header keeps the key out of the address, so it does not end up in logs and browser history.

  4. 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

  1. 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

  2. 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

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.

Lines of code per language, to scale. Blue: one HTTP request. Amber: a headless browser with an official library. No amber bar means no official library.
Eight languages compared: official headless-browser library, HTTP client, lines of code in both tracks, and style
LanguageOfficial headless-browser libraryHTTP client used hereLines, track ALines, track BAs written here
curlOfficial headless-browser libraryNone. curl sends requests; it does not drive a browserHTTP client used herecurlthe tool itselfLines, track AnoneLines, track B6As written herea command
Node.jsOfficial headless-browser libraryPlaywright, PuppeteerHTTP client used herefetchstandard libraryLines, track A6 (Playwright), 6 (Puppeteer)Lines, track B10As written hereasync
PythonOfficial headless-browser libraryPlaywrightHTTP client used hererequestsone packageLines, track A7 (Playwright)Lines, track B12As written heresync
PHPOfficial headless-browser libraryNone from Playwright or Puppeteer. Community: php-webdriverHTTP client used herecurl extensionstandard libraryLines, track AnoneLines, track B12As written heresync
GoOfficial headless-browser libraryNone from Playwright or Puppeteer. Community: chromedpHTTP client used herenet/httpstandard libraryLines, track AnoneLines, track B31As written heresync
RubyOfficial headless-browser libraryNone from Playwright or Puppeteer. Selenium has a Ruby bindingHTTP client used herenet/httpstandard libraryLines, track AnoneLines, track B13As written heresync
JavaOfficial headless-browser libraryPlaywrightHTTP client used herejava.net.http.HttpClientstandard libraryLines, track A14 (Playwright)Lines, track B18As written heresync
C#Official headless-browser libraryPlaywrightHTTP client used hereHttpClientstandard libraryLines, track A6 (Playwright)Lines, track B7As 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.

  1. 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 with image/.

    Node.js
    if (!response.ok) {
      const error = await response.json()
      throw new Error(`${response.status} ${error.error_code}: ${error.error_message}`)
    }
  2. 2Set a timeout longer than the render

    A render can take as long as the timeout option 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)
    end
  3. 3Retry 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-After header 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))
  4. 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.png
  5. 5Stream 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 localhost or 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: fetch in Node.js, net/http in Go and Ruby, HttpClient in Java and C#, and curl in a terminal.
Query string
The part of an address after the ?, a list of name=value pairs 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, so https://example.com inside a query string becomes https%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-Key on the way in and Content-Type on 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. [1]Playwright, Screenshots (Node.js). Checked .
  2. [2]Playwright, Screenshots (Python). Checked .
  3. [3]Playwright, Screenshots (Java). Checked .
  4. [4]Playwright, Screenshots (.NET). Checked .
  5. [5]Puppeteer, Screenshots guide. Checked .
  6. [6]Puppeteer, ScreenshotOptions interface. Checked .
  7. [7]chromedp on GitHub, chromedp README. Checked .
  8. [8]Selenium, Working with windows and tabs: TakeScreenshot. Checked .
  9. [9]php-webdriver on GitHub, php-webdriver README. Checked .
  10. [10]Oracle, HttpResponse.BodyHandlers (Java SE 21). Checked .
  11. [11]Oracle, HttpRequest (Java SE 21). Checked .
  12. [12]Oracle, URLEncoder (Java SE 21). Checked .
  13. [13]Microsoft Learn, HttpClient.GetByteArrayAsync method. Checked .

Screenshot any page in one step

Paste an address into CurlShot and download the screenshot. A free account gives you 100 screenshots every month. No card needed.