TutorialWebhooksAsync

Screenshot API Webhooks: Real-Time Notifications Tutorial

Published March 14, 2026 -- 10 min read

Synchronous screenshot capture works for simple use cases, but when you need to process hundreds of URLs or capture slow-loading pages, async processing with webhooks is the way to go. This tutorial shows you how.

Why Async + Webhooks?

How It Works

  1. Submit async request: POST to /v1/screenshot/async with a callback_url
  2. Get job ID immediately: The API returns a job ID and status URL within milliseconds
  3. Screenshot renders in background: The API captures the page asynchronously
  4. Webhook fires on completion: Your callback URL receives the screenshot data

Step 1: Submit an Async Request

curl -X POST https://screenshotapi-api-production.up.railway.app/v1/screenshot/async \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://stripe.com",
    "width": 1280,
    "height": 800,
    "format": "png",
    "device": "desktop",
    "callback_url": "https://your-app.com/webhook/screenshot"
  }'

# Response (immediate):
{
  "id": "job_abc123",
  "status": "pending",
  "status_url": "/v1/status/job_abc123",
  "message": "Screenshot job queued. Poll the status_url or wait for webhook callback."
}

Step 2: Set Up Your Webhook Endpoint

Create an endpoint that receives POST requests with the screenshot data. Here is a Node.js/Express example:

import express from 'express';
import fs from 'fs';

const app = express();
app.use(express.json({ limit: '50mb' })); // Screenshots can be large

app.post('/webhook/screenshot', (req, res) => {
  const { id, status, content_type, size_bytes, duration_ms, data_base64, error } = req.body;

  if (status === 'completed') {
    console.log(`Screenshot ${id} completed in ${duration_ms}ms (${size_bytes} bytes)`);

    // Save the screenshot
    const buffer = Buffer.from(data_base64, 'base64');
    const ext = content_type.split('/')[1]; // png, jpeg, webp
    fs.writeFileSync(`screenshots/${id}.${ext}`, buffer);

    // Update your database, send notification, etc.
    // updateScreenshotRecord(id, { status: 'completed', size: size_bytes });
  }

  if (status === 'failed') {
    console.error(`Screenshot ${id} failed: ${error}`);
    // Handle failure: retry, notify admin, etc.
  }

  // Always respond 200 to acknowledge receipt
  res.sendStatus(200);
});

app.listen(3000);

Step 3: Poll Status (Alternative to Webhooks)

If you cannot set up a webhook endpoint, you can poll the status URL:

async function waitForScreenshot(jobId) {
  const API_BASE = 'https://screenshotapi-api-production.up.railway.app';

  while (true) {
    const res = await fetch(`${API_BASE}/v1/status/${jobId}`, {
      headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
    });
    const data = await res.json();

    if (data.status === 'completed') {
      // Download the completed screenshot
      const imgRes = await fetch(`${API_BASE}/v1/status/${jobId}/download`, {
        headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
      });
      return Buffer.from(await imgRes.arrayBuffer());
    }

    if (data.status === 'failed') {
      throw new Error(`Screenshot failed: ${data.error}`);
    }

    // Wait 2 seconds before checking again
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
}

Webhook Payload Reference

Success Payload

{
  "id": "job_abc123",
  "status": "completed",
  "content_type": "image/png",
  "size_bytes": 245760,
  "duration_ms": 4523,
  "data_base64": "iVBORw0KGgoAAAANSUhEUg..."
}

Failure Payload

{
  "id": "job_abc123",
  "status": "failed",
  "error": "Page load timed out"
}

Batch Processing Pattern

Process hundreds of URLs efficiently by submitting async requests in bulk:

const urls = [
  'https://stripe.com',
  'https://github.com',
  'https://vercel.com',
  // ... hundreds more
];

const API_BASE = 'https://screenshotapi-api-production.up.railway.app';

// Submit all jobs
const jobs = await Promise.all(urls.map(async (url) => {
  const res = await fetch(`${API_BASE}/v1/screenshot/async`, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url,
      format: 'png',
      device: 'desktop',
      callback_url: 'https://your-app.com/webhook/screenshot',
    }),
  });
  return res.json();
}));

console.log(`Submitted ${jobs.length} screenshot jobs`);
// All results arrive via webhook -- no polling needed

Best Practices

  1. Verify webhook signatures: In production, verify that webhook requests actually come from ScreenshotAPI
  2. Respond quickly: Your webhook endpoint should respond within 5 seconds. Process data asynchronously if needed.
  3. Handle retries: Webhooks may be sent more than once. Use the job ID to deduplicate.
  4. Store data immediately: Save the base64 data to disk or cloud storage right away. Do not keep it in memory.
  5. Rate limit your submissions: When doing batch processing, submit 10-50 jobs at a time, not all at once.

Try async screenshots

Async processing with webhooks is available on all plans. Get started with 100 free screenshots per month.

Related Articles