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.