Menu
Sign up

Website screenshots from the command line with curl: every option, tested

Take website screenshots from a terminal or a shell script with curl: viewport, full page, phone, PDF, async jobs, signed URLs signed with openssl.

Published November 8, 2026 · 5 min read

A screenshot from the command line is handy in shell scripts, cron jobs, CI steps and one-off checks: one line, one file. Chrome can do it on its own machine (chrome --headless --screenshot=shot.png https://example.com/), but then you install and update Chrome, its fonts and its sandbox wherever the script runs. With a screenshot API the script needs only curl. This page shows a tested curl command for each common job.

How we tested. Every command below was run on 2026-11-08 in bash with curl and jq 1.6 against our API on our development stack. Times are the full command, page load included.

Setup

export API_KEY=...            # from your dashboard
export API=https://shareglint.com

Every request is a POST /v1/screenshots with a JSON body. ?wait=true holds the connection until the capture is done and answers with JSON that holds the file's url.

The first screen of a page

curl -s -X POST "$API/v1/screenshots?wait=true" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/"}' \
  | jq -r .url | xargs curl -s -o example.png

Result: a 1280×800 PNG of 71 KB, in 2.4 s. The default viewport is 1280×800. Running the same command again took 0.1 s: the same options within 24 hours return the stored file and cost nothing.

The whole page

curl -s -X POST "$API/v1/screenshots?wait=true" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.python.org/", "full_page": true, "lazy_load": true, "format": "jpeg", "block_cookie_banners": true}' \
  | jq -r .url | xargs curl -s -o python-full.jpg

A 1280×2726 JPEG of 372 KB. lazy_load scrolls through the page first so images further down load; block_cookie_banners accepts or hides consent banners (what it did on 12 sites). Full pages are capped at 16,000 px tall.

A phone, a PDF, a video

Change the body, keep the rest of the command:

Body What you get
{"url": "…", "device": "iphone-15"} 1179×2556 PNG, iPhone user agent (all presets)
{"url": "…", "device": "mobile"} 390×844 PNG, the same kind of phone at 1×
{"url": "…", "format": "pdf"} Chrome's print of the page; example.com gave a 1-page PDF of 124 KB
{"url": "…", "dark_mode": true} The site's dark theme, if it has one (16 sites tested)
{"url": "…", "scroll_video": true} A smooth scroll as MP4 (scroll videos)
{"url": "…", "selector": "main"} One element only
{"url": "…", "hide_selectors": ["#chat", ".promo"]} The page with those elements hidden

Use format "jpeg" or "webp" for smaller files, "quality" (1–100) to trade size for sharpness.

Without waiting: start a job, poll it

For slow pages or many URLs, do not hold the connection. Leave out ?wait=true, keep the id, and ask for it:

ID=$(curl -s -X POST "$API/v1/screenshots" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.rust-lang.org/", "device": "pixel-8"}' | jq -r .id)

until STATUS=$(curl -s "$API/v1/screenshots/$ID" -H "Authorization: Bearer $API_KEY" | jq -r .status) \
      && [ "$STATUS" = completed ] || [ "$STATUS" = failed ]; do
  sleep 1
done
curl -s "$API/v1/screenshots/$ID" -H "Authorization: Bearer $API_KEY" | jq -r .url | xargs curl -s -o rust.png

The first answer had "status": "queued"; the job completed after four polls, 3.5 s in all. Instead of polling, pass "webhook_url" and we call your endpoint when it is done (bulk screenshots shows both).

A failed job says why. One of our test captures, w3.org with a phone user agent, came back failed with "error": "The site answered with a bot check (HTTP 403) that did not let the browser through." — a site's bot protection, not something a retry fixes.

Errors in a script

Validation errors come back at once, with HTTP 422 and a message you can print:

$ curl ... -d '{"url": "http://localhost/"}'
{"message":"That address is not publicly reachable. Screenshots can only capture public pages.", ...}   HTTP 422

$ curl ... -d '{"url": "https://example.com/", "device": "nokia"}'
{"message":"`device` must be one of: desktop, laptop, tablet, mobile, iphone-15, ...", ...}   HTTP 422

Add -w '%{http_code}' (or --fail-with-body) so the script can stop on them. Retrying a POST is safe with an Idempotency-Key header: the same key and body return the first response instead of capturing again.

curl only: a signed URL

No JSON, no jq, no API key in the script: a signed GET /s/ URL, which redirects to the finished file. It needs your account id and signing secret, and the signature can be made with openssl:

#!/usr/bin/env bash
# Usage: ./sign.sh URL [name=value ...]  → prints a signed screenshot URL
set -euo pipefail
page=$1; shift
enc() { jq -rn --arg v "$1" '$v|@uri'; }          # RFC 3986, like the server
query=$( { echo "url=$(enc "$page")"; for p in "$@"; do echo "${p%%=*}=$(enc "${p#*=}")"; done; } | LC_ALL=C sort | paste -sd'&')
path="/s/${SHOT_ACCOUNT_ID}.png"
s=$(printf '%s' "$path?$query" | openssl dgst -sha256 -hmac "$SHOT_SIGNING_SECRET" | awk '{print $NF}' | cut -c1-32)
echo "${BASE_URL}${path}?${query}&s=${s}"
curl -sL -o debian.png "$(./sign.sh https://www.debian.org/ device=mobile block_cookie_banners=1 'hide_selectors=#footer, .ad')"

The first request answered 302 after 6.6 s of capturing; with -L the second one fetched the stored 390×844 PNG in 0.1 s. Lists such as hide_selectors are one comma-separated value in a URL. The same URL works in an <img> tag or a Markdown file; the scheme is explained in signed image URLs. This sorter assumes parameter names without special characters, which all of ours are.

On a schedule

Put any of these commands in cron or a CI job and you have a daily screenshot of a page. Save each one under a dated name, and keep in mind that the cache returns the same file for 24 hours: one capture a day per set of options. Scheduled website screenshots has the cron line and a GitHub Actions workflow.

Cost

A viewport capture is 1 render of the quota, a full page 2, a scroll video 5; cached repeats and failed captures cost nothing (pricing). The jq manual's @uri filter and OpenSSL's dgst command are the two non-curl tools above.

FAQ

How do I take a screenshot of a website from the command line?

Send the URL to a screenshot API with curl and save the file it returns. With ours: curl -X POST with ?wait=true and a JSON body such as {"url": "https://example.com/"} returns JSON with the image URL; pipe it through jq -r .url and curl -o to save it. Headless Chrome itself also has a --screenshot flag if you want no service at all.

Can I screenshot a website with curl alone, without jq?

Yes, with a signed GET URL: curl -L -o shot.png "<signed url>" follows the redirect to the finished file. The URL is signed with your signing secret; the signature is an HMAC-SHA256 you can compute with openssl in the same script.

How long does a command-line screenshot take?

As long as the page takes to load and settle, plus little else. example.com came back in 2.4 s; the same request again took 0.1 s because an identical request within 24 hours returns the stored file.