Take website screenshots from the command line

Capture website screenshots with curl or the ScreenshotOne CLI. Save PNG files, process a list of URLs, handle failures, and schedule repeat captures.

Blog post5 min read

Written by

Dmytro Krasun

Updated on

The quickest way to screenshot a website from a terminal is to send the URL to a screenshot API and save what comes back. The browser runs on the API’s servers, so all you need locally is curl, with no Chrome, Puppeteer, or missing system libraries to deal with.

This guide uses Bash and curl on macOS or Linux. Everything also works in WSL on Windows. If you want to run the browser on your own machine instead, see the Puppeteer screenshot guide.

One URL, one command

Get a ScreenshotOne access key, export it as SCREENSHOTONE_ACCESS_KEY, and run:

Terminal window
curl --fail --silent --show-error --max-time 120 --get \
'https://api.screenshotone.com/take' \
--header "X-Access-Key: ${SCREENSHOTONE_ACCESS_KEY:?Set the access key first}" \
--data-urlencode 'url=https://example.com/' \
--data-urlencode 'format=png' \
--data-urlencode 'full_page=true' \
--data-urlencode 'viewport_width=1280' \
--data-urlencode 'viewport_height=800' \
--output example.png

That’s it. example.png is your screenshot. The response is the image itself, with no JSON to parse.

A few of these flags matter more than they look:

  • --data-urlencode encodes the target URL correctly even if it has its own ?a=1&b=2 query string.
  • The single quotes stop your shell from treating & as “run in background.”
  • --fail makes HTTP errors return a non-zero exit code. Without it, curl happily saves the error response as example.png, and you find out later.

A reusable script that won’t overwrite good captures

For repeat runs, you want one more property: if a capture fails, keep the last good image. This script downloads to a temporary file and only replaces the real one once the result looks valid. Save it as capture-one.sh:

#!/usr/bin/env bash
set -euo pipefail
: "${SCREENSHOTONE_ACCESS_KEY:?Set SCREENSHOTONE_ACCESS_KEY}"
if [[ $# -lt 1 || $# -gt 2 ]]; then
printf 'Usage: bash capture-one.sh URL [output.png]\n' >&2
exit 2
fi
target_url=$1
output_path=${2:-screenshot.png}
mkdir -p -- "$(dirname -- "$output_path")"
temporary_path=$(mktemp "${output_path}.tmp.XXXXXX")
trap 'rm -f -- "$temporary_path"' EXIT
content_type=$(curl --fail --silent --show-error --max-time 120 --get \
'https://api.screenshotone.com/take' \
--header "X-Access-Key: $SCREENSHOTONE_ACCESS_KEY" \
--data-urlencode "url=$target_url" \
--data-urlencode 'format=png' \
--data-urlencode 'full_page=true' \
--data-urlencode 'viewport_width=1280' \
--data-urlencode 'viewport_height=800' \
--output "$temporary_path" \
--write-out '%{content_type}')
if [[ $content_type != image/png* || ! -s $temporary_path ]]; then
printf 'Did not get a PNG back; kept the existing file.\n' >&2
exit 1
fi
mv -f -- "$temporary_path" "$output_path"
printf 'Saved %s\n' "$output_path"

Run it with a quoted URL:

Terminal window
bash capture-one.sh 'https://example.com/?category=design&sort=new' captures/example.png

Two notes on the capture settings:

  • The viewport width picks the layout. full_page=true only makes the image taller. It won’t give you the desktop layout on its own, so set the width you actually want.
  • Full-page captures scroll the page first so lazy-loaded images have a chance to appear. If some are still missing, increase the scroll delay. See the full-page options.

A list of URLs

Put one URL per line in urls.txt. Lines starting with # are ignored:

https://example.com/
https://example.org/
# Add the public pages you want to capture.

Save this as capture-list.sh, next to capture-one.sh:

#!/usr/bin/env bash
set -euo pipefail
input_file=${1:-urls.txt}
output_dir=${2:-captures}
script_dir=$(cd -- "$(dirname -- "$0")" && pwd)
mkdir -p -- "$output_dir"
manifest="$output_dir/results.tsv"
printf 'file\tstatus\turl\n' > "$manifest"
number=0
failures=0
while IFS= read -r target_url || [[ -n "$target_url" ]]; do
target_url=${target_url%$'\r'}
[[ -z "$target_url" || "$target_url" == \#* ]] && continue
number=$((number + 1))
filename=$(printf '%04d.png' "$number")
if bash "$script_dir/capture-one.sh" "$target_url" "$output_dir/$filename"; then
status=ok
else
status=failed
failures=$((failures + 1))
fi
printf '%s\t%s\t%s\n' "$filename" "$status" "$target_url" >> "$manifest"
done < "$input_file"
if [[ $number -eq 0 ]]; then
printf 'No URLs found in %s.\n' "$input_file" >&2
exit 2
fi
printf 'Captured %s URLs, %s failed.\n' "$number" "$failures"
[[ $failures -eq 0 ]]

Run it into a timestamped folder so runs don’t mix:

Terminal window
bash capture-list.sh urls.txt "captures/$(date -u +%Y%m%dT%H%M%SZ)"

A failed URL doesn’t stop the run. It’s marked failed in results.tsv, the other URLs continue, and the script exits non-zero at the end so cron or CI notices. Use results.tsv to see which file belongs to which URL rather than going by the file names.

The script processes URLs one at a time, which is fine for dozens of URLs. For thousands, check your plan’s concurrency limit, run a few in parallel (for example with xargs -P), and add retries with backoff.

Run it on a schedule

The API takes the screenshots, and something else decides when. That can be cron, systemd timers, or a CI service you already use. With cron on Linux:

0 8 * * * /bin/bash /opt/site-captures/run-daily.sh >> /opt/site-captures/capture.log 2>&1

And run-daily.sh:

#!/usr/bin/env bash
set -euo pipefail
cd /opt/site-captures
# Keep the key in a file only this user can read, outside version control.
source /opt/site-captures/access-key.env
export SCREENSHOTONE_ACCESS_KEY
bash capture-list.sh urls.txt "captures/$(date -u +%Y%m%dT%H%M%SZ)"

Cron catches a lot of people out because it runs with a minimal environment: a short PATH, no shell profile, and the server’s time zone. Use absolute paths, and run the wrapper manually once before you rely on the schedule.

If a run could take longer than the gap between runs, wrap it in flock so two don’t overlap. Make sure failures reach you as alerts. Nobody reads capture.log until something has already gone wrong.

For captures alongside a deployment review, the GitHub Actions screenshot guide shows how to capture deployed routes and save the images as workflow artifacts.

Or use the ScreenshotOne CLI

If you’d rather have named commands than curl flags, install the ScreenshotOne CLI. It’s also available as a Docker image (see the CLI integration page). Once your keys are configured:

Terminal window
screenshotone take 'https://example.com/' -o viewport.png
screenshotone full-page 'https://example.com/' --full-page-scroll -o full-page.png
screenshotone pdf 'https://example.com/' --pdf-print-background -o document.pdf

The CLI signs its requests, so follow its README to set both the access key and the secret key. One gotcha: its --json flag prints the signed request URL. It doesn’t ask the API for JSON metadata the way response_type=json does.

When it doesn’t work

SymptomCheck
Fails before sending anythingIs SCREENSHOTONE_ACCESS_KEY exported in this shell?
401 or 403Is the key correct, and does your account require signed requests?
The PNG shows a login pageThe API doesn’t have your browser’s cookies
Missing images further downIncrease the scroll delay, or wait for a specific selector
Very slowLook at what the page loads, and set timeouts before adding retries
Works manually, fails in cronWorking directory, PATH, or credentials in cron’s environment

My advice: get one URL looking right, then roll the same settings out to the list. A 200 OK only means the API returned an image. You still need to look at it to know the page rendered the way you wanted.

Read more Screenshot rendering

Interviews, tips, guides, industry best practices, and news.

View all posts
Take website screenshots with Python

Take website screenshots with Python

Learn how to take webpage screenshots in Python with Playwright, including full-page screenshots, common fixes, and production-ready API options.

Read more

Automate website screenshots

Exhaustive documentation, ready SDKs, no-code tools, and other automation to help you render website screenshots and outsource all the boring work related to that to us.