# 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 2026-11-08 · https://shareglint.com/en/guides/website-screenshot-curl

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

```bash
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

```bash
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

```bash
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](https://shareglint.com/en/guides/hide-cookie-banners-in-screenshots)). 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](https://shareglint.com/en/guides/mobile-website-screenshot)) |
| `{"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](https://shareglint.com/en/guides/dark-mode-screenshots)) |
| `{"url": "…", "scroll_video": true}` | A smooth scroll as MP4 ([scroll videos](https://shareglint.com/en/guides/scrolling-screenshot-video)) |
| `{"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:

```bash
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](https://shareglint.com/en/guides/bulk-website-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:

```text
$ 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:

```bash
#!/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}"
```

```bash
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](https://shareglint.com/en/guides/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](https://shareglint.com/en/guides/schedule-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](https://shareglint.com/en/pricing)). The jq manual's [`@uri` filter](https://jqlang.org/manual/#format-strings-and-escaping) and OpenSSL's [dgst command](https://docs.openssl.org/master/man1/openssl-dgst/) 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.
