Documentation

Webhooks Guide

Webhooks allow you to receive notifications when async operations complete. This is ideal for long-running captures or high-volume processing.

When to Use Webhooks

Use async endpoints with webhooks when:

  • Capturing pages that take longer than 30 seconds to render
  • Processing many screenshots/PDFs in parallel
  • You want non-blocking API calls
  • Building queue-based processing systems

How It Works

  1. Make an async request - Call /v2/screenshot/async or /v2/pdf/async
  2. Receive a job ID - The API immediately returns a job ID
  3. Processing happens - ScreenCraft processes your request in the background
  4. Webhook fires - When complete, we POST the result to your webhook URL

Setting Up Webhooks

1. Configure Your Webhook URL

Set your webhook URL in the dashboard under Settings > Webhooks. Your endpoint must:

  • Be publicly accessible (HTTPS required)
  • Accept POST requests
  • Return a 2xx status code within 30 seconds

2. Make an Async Request

Request
{
  "url": "https://example.com/complex-page",
  "fullPage": true,
  "webhook": {
    "url": "https://your-server.com/webhook/screencraft",
    "headers": {
      "X-Custom-Header": "your-value"
    }
  }
}

3. Receive the Immediate Response

Response
{
  "success": true,
  "data": {
    "job_id": "job_abc123xyz789",
    "status": "processing",
    "estimated_time": 15,
    "webhook_url": "https://your-server.com/webhook/screencraft"
  }
}

4. Handle the Webhook

When processing completes, we POST to your webhook URL:

Webhook Payload (Success)
{
  "event": "screenshot.completed",
  "job_id": "job_abc123xyz789",
  "timestamp": "2025-12-27T10:30:45Z",
  "data": {
    "id": "scr_def456ghi789",
    "url": "https://cdn.screencraft.dev/s/abc123.png",
    "width": 1920,
    "height": 4500,
    "format": "png",
    "size": 1245678,
    "created_at": "2025-12-27T10:30:45Z",
    "expires_at": "2025-12-28T10:30:45Z"
  }
}
Webhook Payload (Failed)
{
  "event": "screenshot.failed",
  "job_id": "job_abc123xyz789",
  "timestamp": "2025-12-27T10:30:45Z",
  "error": {
    "code": "TIMEOUT",
    "message": "Page took too long to load",
    "details": {
      "url": "https://example.com/slow-page",
      "timeout_ms": 30000
    }
  }
}

Webhook Events

Event Description
screenshot.completed Screenshot generated successfully
screenshot.failed Screenshot generation failed
pdf.completed PDF generated successfully
pdf.failed PDF generation failed

Verifying Webhooks

All webhooks include a signature header for verification. Always verify signatures to ensure the request came from ScreenCraft.

Signature Header

Headers
X-ScreenCraft-Signature: sha256=abc123def456...
X-ScreenCraft-Timestamp: 1703673045

Verification Code

verify.js (Node.js)
const crypto = require('crypto');

function verifyWebhook(payload, signature, timestamp, secret) {
  // Check timestamp to prevent replay attacks (5 min window)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    throw new Error('Webhook timestamp too old');
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Compare signatures (timing-safe)
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error('Invalid webhook signature');
  }

  return true;
}

// Express.js example
app.post('/webhook/screencraft', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-screencraft-signature'];
  const timestamp = req.headers['x-screencraft-timestamp'];

  try {
    verifyWebhook(req.body.toString(), signature, timestamp, process.env.WEBHOOK_SECRET);

    const event = JSON.parse(req.body);
    console.log('Received event:', event.event, event.job_id);

    // Process the event...

    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook verification failed:', error);
    res.status(400).send('Invalid signature');
  }
});
verify.py (Python)
import hmac
import hashlib
import time

def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
    # Check timestamp (5 min window)
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        raise ValueError('Webhook timestamp too old')

    # Compute expected signature
    signed_payload = f"{timestamp}.{payload.decode()}"
    expected_signature = 'sha256=' + hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Compare signatures
    if not hmac.compare_digest(signature, expected_signature):
        raise ValueError('Invalid webhook signature')

    return True

# Flask example
@app.route('/webhook/screencraft', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-ScreenCraft-Signature')
    timestamp = request.headers.get('X-ScreenCraft-Timestamp')

    try:
        verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET)
        event = request.json
        print(f"Received: {event['event']} - {event['job_id']}")
        return 'OK', 200
    except ValueError as e:
        return str(e), 400

Retry Policy

If your webhook endpoint returns a non-2xx status code, we retry with exponential backoff:

Attempt Delay
1 Immediate
2 1 minute
3 5 minutes
4 30 minutes
5 2 hours

After 5 failed attempts, the webhook is marked as failed. You can view failed webhooks in your dashboard.

Polling Alternative

If you prefer not to use webhooks, you can poll for job status:

Request
curl https://api.screencraft.dev/v2/jobs/job_abc123xyz789 \
  -H "Authorization: Bearer sk_live_your_api_key"
Response
{
  "success": true,
  "data": {
    "job_id": "job_abc123xyz789",
    "status": "completed",
    "result": {
      "url": "https://cdn.screencraft.dev/s/abc123.png",
      "width": 1920,
      "height": 4500
    },
    "created_at": "2025-12-27T10:30:00Z",
    "completed_at": "2025-12-27T10:30:45Z"
  }
}

Best Practices

  • Respond quickly: Return 200 immediately, process asynchronously
  • Handle duplicates: Use job_id for idempotent processing
  • Verify signatures: Always validate webhook authenticity
  • Use HTTPS: Never expose webhook endpoints over HTTP
  • Log everything: Log all webhooks for debugging
  • Set timeouts: Configure appropriate timeouts on your server