Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Dark Mode Image Format & Quality MCP Server Webhook Pricing Docs Blog Log In Sign Up

GitHub Actions Screenshot API — Automate Visual Checks in CI/CD

Every push to main triggers your test suite, your linter, maybe a deploy. But nobody checks whether the deploy actually looks right. A broken CSS import, a missing environment variable that blanks out the hero section, a third-party script that injects a banner you've never seen — these pass unit tests just fine because unit tests don't have eyes.

GitHub Actions can take a screenshot of your live page after every deployment and save it as a build artifact. You could install Puppeteer in the workflow to do this, but then you're managing a browser binary, handling timeouts, and troubleshooting rendering issues inside a CI runner. ScreenshotRun's API does the same thing with one curl command.

Your first screenshot in a GitHub Actions workflow

Setup takes about three minutes. You need a ScreenshotRun API key (grab one free from the dashboard, no credit card needed) and a repository with GitHub Actions enabled.

Create a file at .github/workflows/screenshot.yml:

name: Capture Screenshots

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  screenshot:
    runs-on: ubuntu-latest
    steps:
      - name: Take screenshot
        run: |
          curl -s -o screenshot.png \
            -H "Authorization: Bearer ${{ secrets.SCREENSHOTRUN_API_KEY }}" \
            "https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com&format=png&width=1280&block_cookies=true"

      - name: Upload as artifact
        uses: actions/upload-artifact@v4
        with:
          name: screenshot-${{ github.sha }}
          path: screenshot.png
          retention-days: 30

Store your API key in Settings > Secrets and variables > Actions > New repository secret. Name it SCREENSHOTRUN_API_KEY. GitHub encrypts it and masks it in logs, so the key never shows up in your workflow file or build output.

After the workflow runs, download the artifact from the Actions tab. You'll see exactly what your page looked like at that commit. Every artifact is tied to a specific SHA, so if something looks off, you can trace it back to the exact code change.

Screenshot multiple pages with a matrix strategy

One page is useful. Five pages give you actual coverage. GitHub's matrix strategy runs them in parallel without duplicating your workflow file.

jobs:
  screenshots:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        page:
          - { name: homepage, url: "https://yoursite.com" }
          - { name: pricing, url: "https://yoursite.com/pricing" }
          - { name: docs, url: "https://yoursite.com/docs" }
          - { name: login, url: "https://yoursite.com/login" }
    steps:
      - name: Capture ${{ matrix.page.name }}
        run: |
          curl -s -o "${{ matrix.page.name }}.png" \
            -H "Authorization: Bearer ${{ secrets.SCREENSHOTRUN_API_KEY }}" \
            "https://api.screenshotrun.com/v1/screenshots/capture?url=${{ matrix.page.url }}&format=png&width=1280&block_cookies=true&block_ads=true"

      - name: Upload screenshot
        uses: actions/upload-artifact@v4
        with:
          name: screenshot-${{ matrix.page.name }}
          path: "${{ matrix.page.name }}.png"

Four pages, four parallel jobs. Each finishes in 3-5 seconds. Compare that to installing Puppeteer, which takes 15-20 seconds just for npm install on a fresh runner before you've captured anything.

Post-deploy visual checks: catch what tests miss

The most useful screenshot is the one taken right after deployment. Not against localhost or a staging preview, but against production — from the outside, the way your actual users see it.

This workflow runs automatically after your deploy job finishes:

name: Post-Deploy Screenshots

on:
  workflow_run:
    workflows: ["Deploy to Production"]
    types: [completed]

jobs:
  visual-check:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    steps:
      - name: Wait for CDN propagation
        run: sleep 30

      - name: Capture production pages
        run: |
          PAGES="homepage:https://yoursite.com pricing:https://yoursite.com/pricing"

          for entry in $PAGES; do
            name="${entry%%:*}"
            url="${entry#*:}"
            curl -s -o "${name}.png" \
              -H "Authorization: Bearer ${{ secrets.SCREENSHOTRUN_API_KEY }}" \
              "https://api.screenshotrun.com/v1/screenshots/capture?url=${url}&format=png&width=1280&full_page=true&block_cookies=true&delay=2000"
          done

      - name: Upload all screenshots
        uses: actions/upload-artifact@v4
        with:
          name: production-screenshots-${{ github.event.workflow_run.head_sha }}
          path: "*.png"
          retention-days: 90

The sleep 30 looks clunky, but CDN caches need time to clear. Without it, you'll screenshot the old version and wonder why nothing changed. The delay=2000 gives pages with a lot of JavaScript two extra seconds to finish loading before the screenshot is taken.

I ran something like this on a client project last year. On the third week it caught a missing Google Fonts import that made the entire pricing page show up in Times New Roman. Unit tests? All green. End-to-end tests? They ran inside a Docker container where the fonts were already installed. Only the production screenshot — taken from a clean Linux server with no local fonts — showed the real problem. Green tests prove behavior, not appearance.

Scheduled screenshots: monitor your site on a cron

Deployments aren't the only time pages break. Third-party scripts update themselves. SSL certificates expire. A CMS editor publishes a draft page by accident on a Friday evening. GitHub Actions supports cron triggers, and pairing them with screenshot capture gives you a simple website monitoring setup for free.

on:
  schedule:
    - cron: '0 8 * * 1-5'  # weekdays at 8 AM UTC

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - name: Screenshot critical pages
        run: |
          DATE=$(date +%Y-%m-%d)
          mkdir -p screenshots

          URLS="https://yoursite.com https://yoursite.com/pricing https://yoursite.com/checkout"

          for url in $URLS; do
            SLUG=$(echo "$url" | sed 's|https\?://||;s|/|_|g')
            curl -s -o "screenshots/${SLUG}_${DATE}.png" \
              -H "Authorization: Bearer ${{ secrets.SCREENSHOTRUN_API_KEY }}" \
              "https://api.screenshotrun.com/v1/screenshots/capture?url=${url}&format=png&width=1280&block_cookies=true&block_ads=true"
          done

      - name: Upload daily screenshots
        uses: actions/upload-artifact@v4
        with:
          name: daily-monitor-${{ github.run_id }}
          path: screenshots/
          retention-days: 30

Three screenshots every weekday morning. On the free tier (200 captures/month), that's 60 per month with room to spare. The artifacts build up into a visual timeline of your site. When something looks off on a Thursday, scroll back through the artifacts to find the day it changed.

If you need real-time alerts (like posting to Slack when a page changes), that's a different setup. The cron approach here is the baseline: cheap, no extra tools, and it catches the big problems.

Desktop and mobile in one workflow

A page that looks fine at 1280px can easily break on a 390px mobile screen. You can capture both in the same workflow with one extra API call and no additional setup.

    strategy:
      matrix:
        viewport:
          - { name: desktop, width: 1280, device: "" }
          - { name: mobile, width: 390, device: "&device=mobile" }
    steps:
      - name: Capture ${{ matrix.viewport.name }}
        run: |
          curl -s -o "${{ matrix.viewport.name }}.png" \
            -H "Authorization: Bearer ${{ secrets.SCREENSHOTRUN_API_KEY }}" \
            "https://api.screenshotrun.com/v1/screenshots/capture?url=https://yoursite.com&format=png&width=${{ matrix.viewport.width }}${{ matrix.viewport.device }}&block_cookies=true"

Worth knowing: device=mobile does more than shrink the window. It also sets a mobile User-Agent and simulates touch input, so sites that serve different HTML to phones will show their actual mobile version. If you just set width=390 without the device parameter, you get a narrow desktop layout instead, which isn't the same thing.

API parameters that work well in CI/CD

Not every parameter matters in a GitHub Actions context. These are the ones I'd actually use.

ParameterValueWhat it does in CI
full_pagetrueCaptures the entire scrollable page, not just the visible window. Good for catching bugs below the fold.
delay2000-5000Waits before capturing. Most pages with a lot of JavaScript need 2 seconds. Pages pulling data from external APIs may need more.
wait_for_selectorCSS selectorWaits until a specific element appears on the page. More precise than delay — use something like .main-content.
block_cookiestrueHides cookie consent banners. About 40% of European sites show these, and they'll cover most of your screenshot otherwise.
block_adstrueRemoves ad banners. Gives you cleaner screenshots for comparison.
dark_modetrueCaptures the dark theme version of the page. If your site supports dark mode, test both.
cache_ttl0Turns off caching. In CI you always want a fresh screenshot, not a cached version from a previous run.
stealthtrueHelps get past bot detection. Some sites (including yours, if you use Cloudflare) block automated browsers by default.
formatpng/webpPNG for pixel-perfect comparison. WebP for smaller file sizes when storage matters.

The full list is in the API docs. For CI, I'd start with format=png&width=1280&block_cookies=true&cache_ttl=0 and add more parameters only when you run into a specific issue.

Why not just install Puppeteer in the workflow?

You can. Many teams do. But there are a few things that make it annoying in GitHub Actions specifically.

First, the install is slow. The puppeteer npm package downloads a Chromium browser (~170MB). On a fresh runner that takes 15-20 seconds before your script even starts. An API call takes 2-3 seconds total with nothing to install.

Second, system libraries. Ubuntu runners come with most of what Chromium needs, but not everything. If libgbm or libasound2 is missing, Chromium crashes silently. I've spent more debugging time than I'd like on error while loading shared libraries messages, trying to figure out which package to apt-get install. With an API call, none of that is your problem.

Third, fonts look different. Linux doesn't have the same fonts as macOS or Windows. Helvetica gets replaced with Liberation Sans, and system-ui picks a different font entirely. Your CI screenshots will never match what your designer sees on their Mac. The API runs browsers with a font set designed for consistent output across platforms.

That said, Puppeteer is the right choice when you need to log into pages with session cookies, or when you need to screenshot localhost during the build. The API works with public URLs only — it can't reach your runner's local network. The Puppeteer vs screenshot API comparison goes deeper on the tradeoffs.

Where to send screenshots after capture

Build artifacts are a good start, but you can do more with the screenshots once you have them.

The quickest win is posting them as PR comments. The peter-evans/create-or-update-comment action lets you attach an image to the pull request that triggered the build. Upload the screenshot somewhere accessible (an S3 bucket or a public artifact URL) and reference it as markdown in the comment. Reviewers see the visual change without downloading anything.

Slack is even easier. One curl command to a Slack incoming webhook drops the screenshot into your deploy channel. Pair it with the post-deploy workflow above and your team sees a picture of every release as it goes out.

For permanent storage, push screenshots to S3 instead of relying on GitHub artifacts, which expire after 90 days. The aws-actions/configure-aws-credentials action handles AWS login, and then a simple aws s3 cp with a date-stamped path gives you an archive that sticks around. Useful for compliance or web archiving needs.

One thing to watch: if a page takes longer than 30 seconds to load, the API will time out by default. You can raise that with the timeout parameter, or use the webhook approach to get notified when the screenshot is ready. For most CI workflows though, the default 30-second timeout is more than enough.

The API call itself is the same everywhere. A curl command in GitHub Actions, a request from n8n, a function in Python — the endpoint and parameters don't change. Your integration changes, the API stays the same.

Add visual checks to your CI pipeline

The free tier gives you 200 screenshots per month — enough for daily monitoring plus post-deploy captures. Paste the workflow YAML above, add your API key to repository secrets, and the first screenshot lands on your next commit.

Get Your Free API Key

Frequently asked questions

Yes. It's a regular HTTPS endpoint, so any runner that can make outbound web requests can call it. No IP restrictions, no region limits. If you're on a self-hosted runner behind a firewall, you just need outbound access to api.screenshotrun.com on port 443.

If the page uses cookie-based or header-based auth, yes. The API accepts custom cookies and headers parameters, so you can pass session tokens along with your request. For pages behind HTTP basic auth, put the credentials in the headers parameter. For login forms that need to be filled out, the API won't work on its own — you'd need a browser for that. The behind-login guide covers both approaches.

The API's default timeout is 30 seconds. If the page takes longer, you'll get a 408 error. You can increase it by adding timeout=60000 (60 seconds) to the request. If even that isn't enough, the webhook parameter lets you start the capture and get notified when it's done. For flaky pages, wrapping the curl in a simple retry loop (2-3 attempts with a short pause between) works well.

200 per month. That covers daily monitoring of 5 pages on weekdays (around 100 captures) plus post-deploy screenshots on every push. If your team pushes several times a day with 4-page matrix captures, you'll use it up faster — paid plans start at $9/month for higher volumes.

Yes, and it's actually where the API helps most. Installing a full browser on a self-hosted runner means dealing with system dependencies, browser updates, and memory usage on your own machines. The API handles all the rendering on its side. Your runner just needs curl and internet access.

The API takes the screenshots, but the comparison part is separate. After capturing, download the current screenshot and a baseline version (from a previous artifact or stored in your repo), then run a diff tool like pixelmatch or odiff in a follow-up workflow step. If the difference is too big, fail the build or post a warning on the PR. The visual regression testing page has the full walkthrough.