Screenshot API for Documentation: The Complete Implementation Guide
A hands-on guide to building documentation screenshot automation. Keep your docs always up-to-date with automated capture pipelines.
Documentation screenshots that don't match your product erode user trust and increase support tickets. Yet keeping screenshots manually updated is time-consuming and error-prone, especially with frequent releases.
The solution is treating documentation screenshots as code: automated capture triggered by your CI/CD pipeline, version-controlled alongside your docs, and always in sync with your product.
Why Automate Documentation Screenshots?
The Manual Problem
Consider a typical documentation site:
- 100+ pages with screenshots
- Weekly releases with UI changes
- 3 environments (staging, production, versioned docs)
- Multiple breakpoints (desktop, mobile)
Manual updates require:
- Identify which screenshots need updating
- Navigate to each page
- Capture screenshots with consistent settings
- Crop and resize images
- Update documentation files
- Review and publish
For 100 pages with weekly releases, this becomes a full-time job.
The Automated Solution
Automated screenshots provide:
- Always current: Screenshots update on every deployment
- Consistent quality: Same device, viewport, and timing
- Zero manual effort: Capture happens automatically
- Version control: Screenshots tracked with code
- Multi-environment: Production, staging, and feature branches
Architecture Overview
A documentation screenshot system has three components:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Manifest │ ──▶ │ Capture │ ──▶ │ Storage │
│ (pages.json) │ │ (API calls) │ │ (git/CDN) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ CI/CD │
│ (triggers) │
└─────────────────┘
- Manifest: JSON file listing pages and capture settings
- Capture Script: Code that calls screenshot API for each page
- Storage: Git repository or CDN for images
- CI/CD: Pipeline that triggers capture on deployments
Implementation
Step 1: Create the Manifest
Define what to capture in a JSON file:
// docs/screenshots/manifest.json
{
"baseUrl": "https://app.yourproduct.com",
"outputDir": "docs/images/screenshots",
"defaultOptions": {
"device": "desktop",
"format": "png",
"aiRemoval": true
},
"pages": [
{
"path": "/dashboard",
"filename": "dashboard",
"description": "Main dashboard view"
},
{
"path": "/settings",
"filename": "settings",
"description": "User settings page"
},
{
"path": "/reports",
"filename": "reports",
"description": "Analytics reports",
"waitFor": "#chart-loaded"
},
{
"path": "/editor",
"filename": "editor",
"description": "Document editor",
"viewport": { "width": 1400, "height": 900 }
}
]
}
Step 2: Build the Capture Script
// scripts/capture-screenshots.js
const fs = require('fs');
const path = require('path');
const API_KEY = process.env.SCREENSHOTLY_API_KEY;
const API_URL = 'https://api.screenshotly.app/screenshot';
async function loadManifest() {
const manifestPath = path.join(__dirname, '../docs/screenshots/manifest.json');
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
}
async function captureScreenshot(url, options) {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
device: options.device || 'desktop',
format: options.format || 'png',
viewport: options.viewport,
delay: options.delay || 1000,
aiRemoval: options.aiRemoval ? {
enabled: true,
types: ['cookie-banner', 'chat-widget', 'popup'],
} : undefined,
waitFor: options.waitFor,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Capture failed: ${response.status} - ${error}`);
}
return Buffer.from(await response.arrayBuffer());
}
async function main() {
const manifest = await loadManifest();
const { baseUrl, outputDir, defaultOptions, pages } = manifest;
// Ensure output directory exists
const fullOutputDir = path.join(__dirname, '..', outputDir);
fs.mkdirSync(fullOutputDir, { recursive: true });
console.log(`Capturing ${pages.length} screenshots...`);
const results = [];
for (const page of pages) {
const url = `${baseUrl}${page.path}`;
const filename = `${page.filename}.png`;
const filepath = path.join(fullOutputDir, filename);
console.log(` Capturing: ${page.path}`);
try {
const options = { ...defaultOptions, ...page };
const screenshot = await captureScreenshot(url, options);
fs.writeFileSync(filepath, screenshot);
results.push({
path: page.path,
filename,
status: 'success',
});
} catch (error) {
console.error(` Error: ${error.message}`);
results.push({
path: page.path,
filename,
status: 'failed',
error: error.message,
});
}
}
// Summary
const successful = results.filter(r => r.status === 'success').length;
const failed = results.filter(r => r.status === 'failed').length;
console.log(`\nComplete: ${successful} captured, ${failed} failed`);
// Write results for CI
fs.writeFileSync(
path.join(fullOutputDir, 'capture-results.json'),
JSON.stringify(results, null, 2)
);
// Exit with error if any failed
if (failed > 0) {
process.exit(1);
}
}
main();
Step 3: Add Authentication Support
For authenticated pages, pass session tokens:
// scripts/capture-with-auth.js
async function getAuthToken() {
// Login via API to get session token
const response = await fetch(`${process.env.APP_URL}/api/test-auth`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: process.env.TEST_USER_EMAIL,
password: process.env.TEST_USER_PASSWORD,
}),
});
const data = await response.json();
return data.token;
}
async function captureAuthenticatedPage(url, options, token) {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
device: options.device || 'desktop',
format: 'png',
cookies: [
{
name: 'session_token',
value: token,
domain: new URL(url).hostname,
},
],
aiRemoval: {
enabled: true,
types: ['cookie-banner', 'chat-widget'],
},
}),
});
return Buffer.from(await response.arrayBuffer());
}
// In main flow:
const authToken = await getAuthToken();
for (const page of authenticatedPages) {
const screenshot = await captureAuthenticatedPage(
`${baseUrl}${page.path}`,
page,
authToken
);
// Save screenshot...
}
Step 4: CI/CD Integration
GitHub Actions
# .github/workflows/docs-screenshots.yml
name: Update Documentation Screenshots
on:
# Trigger on production deployments
deployment:
types: [completed]
environments: [production]
# Or manual trigger
workflow_dispatch:
# Or on schedule
schedule:
- cron: '0 0 * * 1' # Weekly on Monday
jobs:
capture-screenshots:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Capture screenshots
env:
SCREENSHOTLY_API_KEY: ${{ secrets.SCREENSHOTLY_API_KEY }}
APP_URL: ${{ secrets.APP_URL }}
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
run: node scripts/capture-screenshots.js
- name: Commit updated screenshots
run: |
git config user.name "Screenshot Bot"
git config user.email "bot@yourcompany.com"
git add docs/images/screenshots/
git diff --staged --quiet || git commit -m "docs: Update screenshots [skip ci]"
git push
GitLab CI
# .gitlab-ci.yml
capture-docs-screenshots:
stage: deploy
image: node:20
script:
- npm ci
- node scripts/capture-screenshots.js
- git config user.name "Screenshot Bot"
- git config user.email "bot@yourcompany.com"
- git add docs/images/screenshots/
- git diff --staged --quiet || git commit -m "docs: Update screenshots"
- git push origin HEAD:$CI_COMMIT_REF_NAME
only:
- main
when: on_success
Advanced Patterns
Multi-Environment Screenshots
Capture for different environments:
// manifest-env.json
{
"environments": {
"production": "https://app.yourproduct.com",
"staging": "https://staging.yourproduct.com"
},
"pages": [...]
}
// Capture script modification
const env = process.env.DOCS_ENV || 'production';
const baseUrl = manifest.environments[env];
const outputDir = `docs/images/screenshots/${env}`;
Versioned Documentation
For versioned docs (Docusaurus, etc.):
// Structure for versioned docs
const versions = ['v1', 'v2', 'v3', 'latest'];
for (const version of versions) {
const baseUrl = `https://app.yourproduct.com/${version}`;
const outputDir = `docs/versioned_docs/version-${version}/images`;
await captureForVersion(baseUrl, outputDir);
}
Diff Detection
Only commit if screenshots actually changed:
const crypto = require('crypto');
function getFileHash(filepath) {
if (!fs.existsSync(filepath)) return null;
const content = fs.readFileSync(filepath);
return crypto.createHash('md5').update(content).digest('hex');
}
async function captureIfChanged(url, filepath, options) {
const oldHash = getFileHash(filepath);
const screenshot = await captureScreenshot(url, options);
const newHash = crypto.createHash('md5').update(screenshot).digest('hex');
if (oldHash !== newHash) {
fs.writeFileSync(filepath, screenshot);
return { changed: true, oldHash, newHash };
}
return { changed: false };
}
Parallel Capture
Speed up with concurrent requests:
const pLimit = require('p-limit');
const limit = pLimit(5); // 5 concurrent captures
async function captureAllParallel(pages) {
const promises = pages.map(page =>
limit(() => captureScreenshot(`${baseUrl}${page.path}`, page))
);
return Promise.all(promises);
}
Best Practices
1. Use AI Element Removal
Always enable AI removal for clean screenshots:
aiRemoval: {
enabled: true,
types: ['cookie-banner', 'chat-widget', 'popup'],
}
2. Wait for Dynamic Content
Add appropriate delays or wait conditions:
{
"path": "/analytics",
"delay": 2000, // Wait for charts to load
"waitFor": "#chart-container" // Wait for specific element
}
3. Consistent Viewport
Use the same viewport for all screenshots:
"defaultOptions": {
"viewport": { "width": 1280, "height": 800 }
}
4. Handle Errors Gracefully
Don't fail the entire pipeline on one error:
try {
await captureScreenshot(url, options);
} catch (error) {
console.warn(`Warning: ${page.path} failed, using existing screenshot`);
// Continue with next page
}
5. Add Metadata
Track when screenshots were captured:
// Write metadata file
fs.writeFileSync('docs/images/screenshots/metadata.json', JSON.stringify({
capturedAt: new Date().toISOString(),
environment: process.env.APP_ENV,
commit: process.env.GITHUB_SHA,
pages: results,
}));
Conclusion
Automated documentation screenshots ensure your docs always match your product. The investment in setup pays off immediately:
- Hours saved weekly on manual updates
- Consistent quality across all pages
- Always current screenshots after every deployment
- Reduced support tickets from confused users
Start with a simple manifest and capture script, then expand to handle authentication, multiple environments, and versioned documentation as needed.
Ready to automate your documentation screenshots?
Get your free API key → - 100 free screenshots to get started.
Learn more about documentation screenshot use cases →
About the Author

Asad Ali
Full-Stack Developer and Founder of ZTabs with 8+ years of experience building scalable web applications and APIs. Specializes in performance optimization, SaaS development, and modern web technologies.
Credentials: Founder & CEO at ZTabs, Full-Stack Developer, Expert in Next.js, React, Node.js, and API optimization
Frequently Asked Questions
Should I commit generated screenshots to the docs repo?
Yes. Committing keeps the docs bundle self-contained, makes diffs visible in PRs, and lets writers preview changes locally. For large sites, use Git LFS or a dedicated /assets submodule to avoid bloating the main repo. Auto-commit via a docs-bot keeps the human out of the loop.
How do I capture authenticated pages in CI?
Mint a narrow read-only doc-capture token on your backend, pass it in a Cookie header on the API request, and rotate weekly. Never store real user credentials in CI secrets. The token scope should only allow access to pages that will appear in public docs.
What do I do when the screenshot doesn't match the rendered page?
Ninety percent of mismatches are timing. Set waitUntil: 'networkidle' to hold the capture until the page has finished loading lazy images and async data. For SPAs, add waitForSelector pointing at a known post-hydration element. If still inconsistent, add a 500ms delay as a last resort.
Ready to capture your first screenshot?
Get started with 100 free screenshots. No credit card required.