Screenshot API for GitLab CI/CD

Automate website screenshots in GitLab CI/CD with ScreenshotAPI. Visual regression testing, deploy evidence, and pipeline screenshot workflows.

Last updated: 2026-08-30

Try ScreenshotAPI free

200 free screenshots/month. No credit card required.

Start for free

Automate Screenshots in GitLab CI/CD with ScreenshotAPI

Screenshots in CI/CD pipelines serve two critical functions: visual regression testing to catch UI changes before they reach production, and deploy evidence to document what your site looks like after each release. The traditional approach means installing Chromium on the runner, which adds minutes to your pipeline and hundreds of megabytes to the image.

ScreenshotAPI replaces all of that for your GitLab CI screenshot integration. Simple HTTP requests from your pipeline jobs return pixel-perfect images. No browser installation, no docker-in-docker, no complex runner configuration.

Quick Start

  1. Sign up for ScreenshotAPI and copy your API key. 200 free screenshots per month are included.
  2. Add the key as a masked CI/CD variable in your GitLab project.
  3. Create pipeline jobs that capture screenshots.

Installation

Add the API key as a CI/CD variable:

  1. Go to your project Settings > CI/CD > Variables.
  2. Click Add variable.
  3. Set Key to SCREENSHOTAPI_KEY and Value to your API key.
  4. Check Mask variable and optionally Protect variable.
  5. Click Add variable.

No runner packages or Docker images are required. The lightweight alpine image with curl is sufficient.

Basic Pipeline: Screenshot on Deploy

Capture screenshots after deploying to production:

yaml
# .gitlab-ci.yml stages: - deploy - screenshot deploy_production: stage: deploy script: - echo "Deploying to production..." environment: name: production url: https://yoursite.com capture_screenshots: stage: screenshot image: alpine:latest needs: [deploy_production] before_script: - apk add --no-cache curl script: - | for page in "" "/pricing" "/docs" "/blog"; do name=$(echo "$page" | sed 's/^\///' | sed 's/^$/homepage/') curl -s -o "${name}.png" \ -H "x-api-key: ${SCREENSHOTAPI_KEY}" \ "https://screenshotapi.to/api/v1/screenshot?url=https://yoursite.com${page}&width=1440&height=900&type=png" echo "Captured ${name}.png" done artifacts: paths: - "*.png" expire_in: 30 days

Visual Regression Testing

Compare screenshots between the target branch and the merge request:

yaml
# .gitlab-ci.yml visual_regression: stage: test image: node:20-alpine rules: - if: $CI_MERGE_REQUEST_IID script: - node scripts/capture-screenshots.js - node scripts/compare-screenshots.js artifacts: paths: - screenshots/ expire_in: 14 days when: always

Capture Script

javascript
// scripts/capture-screenshots.js const fs = require('node:fs'); const path = require('node:path'); const API_BASE = 'https://screenshotapi.to/api/v1/screenshot'; const API_KEY = process.env.SCREENSHOTAPI_KEY; const BASE_URL = process.env.CI_ENVIRONMENT_URL || 'https://staging.yoursite.com'; const pages = [ { name: 'homepage', path: '/' }, { name: 'pricing', path: '/pricing' }, { name: 'docs', path: '/docs' }, ]; const outputDir = 'screenshots/current'; fs.mkdirSync(outputDir, { recursive: true }); async function capture(page) { const url = `${BASE_URL}${page.path}`; const params = new URLSearchParams({ url, width: '1440', height: '900', type: 'png', waitUntil: 'networkidle2', }); const response = await fetch(`${API_BASE}?${params}`, { headers: { 'x-api-key': API_KEY }, }); if (!response.ok) { console.error(`Failed: ${page.name} (${response.status})`); return; } const buffer = Buffer.from(await response.arrayBuffer()); fs.writeFileSync(path.join(outputDir, `${page.name}.png`), buffer); console.log(`Captured ${page.name} (${buffer.length} bytes)`); } async function main() { await Promise.all(pages.map(capture)); } main();

Comparison Script

javascript
// scripts/compare-screenshots.js const fs = require('node:fs'); const path = require('node:path'); const currentDir = 'screenshots/current'; const baselineDir = 'screenshots/baseline'; if (!fs.existsSync(baselineDir)) { console.log('No baseline found. Current screenshots saved as baseline.'); fs.cpSync(currentDir, baselineDir, { recursive: true }); process.exit(0); } const files = fs.readdirSync(currentDir).filter(f => f.endsWith('.png')); let changed = false; for (const file of files) { const current = fs.readFileSync(path.join(currentDir, file)); const baselinePath = path.join(baselineDir, file); if (!fs.existsSync(baselinePath)) { console.log(`New page: ${file}`); changed = true; continue; } const baseline = fs.readFileSync(baselinePath); if (!current.equals(baseline)) { console.log(`Changed: ${file}`); changed = true; } else { console.log(`Unchanged: ${file}`); } } if (changed) { console.log('\nVisual changes detected. Review screenshots artifacts.'); process.exit(1); }

Review App Screenshots

Capture screenshots of GitLab review apps automatically:

yaml
capture_review_screenshots: stage: test image: alpine:latest rules: - if: $CI_MERGE_REQUEST_IID environment: name: review/$CI_MERGE_REQUEST_IID url: https://review-${CI_MERGE_REQUEST_IID}.yoursite.com before_script: - apk add --no-cache curl script: - | REVIEW_URL="https://review-${CI_MERGE_REQUEST_IID}.yoursite.com" echo "Capturing screenshots of ${REVIEW_URL}" curl -s -o homepage.png \ -H "x-api-key: ${SCREENSHOTAPI_KEY}" \ "https://screenshotapi.to/api/v1/screenshot?url=${REVIEW_URL}&width=1440&height=900&type=png&waitUntil=networkidle2" curl -s -o mobile.png \ -H "x-api-key: ${SCREENSHOTAPI_KEY}" \ "https://screenshotapi.to/api/v1/screenshot?url=${REVIEW_URL}&width=390&height=844&type=png&waitUntil=networkidle2" artifacts: paths: - "*.png" expire_in: 7 days

Multi-Viewport Matrix

Capture screenshots across viewports using GitLab's parallel matrix:

yaml
screenshot_matrix: stage: test image: alpine:latest parallel: matrix: - VIEWPORT: [desktop, tablet, mobile] THEME: [light, dark] before_script: - apk add --no-cache curl script: - | case $VIEWPORT in desktop) WIDTH=1440; HEIGHT=900 ;; tablet) WIDTH=768; HEIGHT=1024 ;; mobile) WIDTH=390; HEIGHT=844 ;; esac curl -s -o "screenshot-${VIEWPORT}-${THEME}.png" \ -H "x-api-key: ${SCREENSHOTAPI_KEY}" \ "https://screenshotapi.to/api/v1/screenshot?url=https://yoursite.com&width=${WIDTH}&height=${HEIGHT}&type=png&colorScheme=${THEME}" artifacts: paths: - "*.png" expire_in: 14 days

Scheduled Monitoring

Run screenshots on a pipeline schedule:

yaml
scheduled_monitoring: stage: monitor image: alpine:latest rules: - if: $CI_PIPELINE_SOURCE == "schedule" before_script: - apk add --no-cache curl script: - mkdir -p screenshots - | TIMESTAMP=$(date +%Y%m%d-%H%M) for page in "" "/pricing" "/docs"; do name=$(echo "$page" | sed 's/^\///' | sed 's/^$/homepage/') curl -s -o "screenshots/${name}-${TIMESTAMP}.png" \ -H "x-api-key: ${SCREENSHOTAPI_KEY}" \ "https://screenshotapi.to/api/v1/screenshot?url=https://yoursite.com${page}&width=1440&height=900&type=png" done artifacts: paths: - screenshots/ expire_in: 90 days

Create the schedule in CI/CD > Schedules. Set it to run every 4-6 hours.

Production Tips

Artifacts

Use GitLab artifacts to store screenshots. Set appropriate expiration times: 7 days for MR screenshots, 30 days for deploy evidence, and 90 days for monitoring archives.

Merge Request Comments

Use the GitLab API to post screenshot links as MR comments. This gives reviewers visual context directly in the merge request.

Runner Efficiency

ScreenshotAPI keeps runner requirements minimal. An alpine:latest image with curl is all you need. No Docker-in-Docker, no specialized runner images, no GPU requirements.

Get 200 free screenshots per month. No credit card required. Sign up free →

Further Reading

Frequently asked questions

Do I need to install Chrome on the GitLab CI runner?

No. ScreenshotAPI handles browser rendering remotely. Your pipeline only needs curl or a lightweight script to make HTTP requests. No Chromium, no docker-in-docker.

Can I capture screenshots of GitLab review apps?

Yes. Use the CI_ENVIRONMENT_URL or your review app URL pattern to pass the preview URL to ScreenshotAPI. This gives you visual evidence for every merge request.

How do I store the API key in GitLab CI?

Add it as a CI/CD variable in your project settings. Set it as masked and protected so it does not appear in job logs and is only available on protected branches.

Can I use screenshots for visual regression testing in GitLab?

Yes. Capture screenshots on every merge request, compare them to baseline images committed in the repository, and fail the pipeline if visual differences exceed a threshold.

Related resources

Start capturing screenshots today

Create a free account and get 200 free screenshots per month to try the API. No credit card required.

How many screenshots will your GitLab pipeline actually use?

The jobs on this page are cheap to run but not free to capture: every request that returns an image spends one credit, and one credit is one screenshot on every plan. Sizing that before you wire it in is the difference between a pipeline you can leave running and one that quietly starts failing. The free tier includes 200 captures a month.

Pipeline jobCaptures per runMonthly volume
Deploy evidence, 3 pages per production deploy320 deploys = 60
Merge request visual regression, 3 pages340 merge requests = 120
Review app screenshots, desktop and mobile240 merge requests = 80
Viewport matrix, 3 viewports across 2 themes620 runs = 120
Scheduled monitoring, 1 page every 4 hours16 runs a day = 180
Scheduled monitoring, 4 pages every 4 hours46 runs a day = 720

The free 200 covers one of those jobs, not a whole pipeline. Deploy evidence plus merge request regression plus a single-page 4-hourly monitor is already about 360 captures a month. Starter is 5,000 screenshots a month at $19, Growth is 25,000 at $49 and adds signed URLs, and Scale is 100,000 at $149. Credit packs are an alternative for bursty pipelines: $9 for 1,000 credits up to $299 for 100,000, and pack credits never expire.

On the free tier a capture past the allowance returns HTTP 402 once your credit balance is empty, so the job fails rather than billing you by surprise. Paid plans bill overage per screenshot instead of stopping. If several matrix jobs request the same URL, or you re-run a pipeline, set cacheTtl so identical requests inside that window return the cached response — cached responses do not count against the monthly allowance. It defaults to 0, so ordinary regression captures stay fresh unless you opt in.

Keep CI captures deterministic

A flaky wait is the most common cause of a visual-regression diff that is not a real change. Leave waitUntil at its default of networkidle2, which tolerates the analytics and long-polling connections most apps keep open, and add waitForSelector pointing at an element that only exists once the real content has rendered. The parameter accepts load, domcontentloaded, networkidle0 and networkidle2 — networkidle0 waits for zero connections for 500 ms and suits fully static pages. A bare networkidle is not a valid value.

How many ScreenshotAPI credits does a GitLab CI pipeline use?

One credit per returned screenshot. Deploy evidence for 3 pages across 20 deploys is 60 captures a month, merge request regression on 3 pages across 40 merge requests is 120, and monitoring a single page every 4 hours is 180. Running all three is roughly 360 captures a month, which is past the 200 included in the free tier.

What happens when a GitLab pipeline goes over the monthly screenshot allowance?

On the free tier a request past the allowance returns HTTP 402 once your credit balance is empty, so the job fails instead of billing you by surprise. Paid plans bill overage per screenshot rather than stopping, and credit-pack credits never expire.

How do I stop CI screenshots from producing false visual-regression diffs?

Keep waitUntil at the default networkidle2 and add waitForSelector pointing at an element that only exists once the real content has rendered. waitUntil accepts load, domcontentloaded, networkidle0 and networkidle2; networkidle is not a valid value.