> ## Documentation Index
> Fetch the complete documentation index at: https://docs.screenshotly.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when screenshots are captured, completed, or fail.

Webhooks let you receive automatic POST notifications when screenshot events occur, instead of polling the API.

## How it works

1. Configure a webhook endpoint in your [dashboard](https://app.screenshotly.dev)
2. Screenshotly sends a POST request to your URL when events occur
3. Your server processes the event and responds with a `2xx` status code

## Event types

| Event                  | Description                                 |
| ---------------------- | ------------------------------------------- |
| `screenshot.created`   | A screenshot capture has been initiated     |
| `screenshot.completed` | A screenshot has been successfully captured |
| `screenshot.failed`    | A screenshot capture has failed             |
| `screenshot.deleted`   | A screenshot has been deleted               |

## Webhook payload

When an event occurs, Screenshotly sends a POST request with a JSON body:

```json theme={null}
{
  "event": "screenshot.completed",
  "event_id": "evt_67b8a1c2d3e4f5a6",
  "timestamp": "2025-05-14T10:00:03.000Z",
  "data": {
    "screenshot": {
      "_id": "67b8a1c2d3e4f5a6b7c8d9e0",
      "url": "https://example.com",
      "status": "completed",
      "imageUrl": "https://cdn.screenshotly.dev/screenshots/67b8a1c2d3e4f5a6b7c8d9e0.png",
      "metadata": {
        "fileSize": 245760,
        "format": "png",
        "dimensions": { "width": 1280, "height": 1024 }
      }
    },
    "user": {
      "id": "67a1b2c3d4e5f6a7b8c9d0e1"
    }
  }
}
```

For `screenshot.failed` events, an `error` field is included:

```json theme={null}
{
  "event": "screenshot.failed",
  "event_id": "evt_67b8a1c2d3e4f5a7",
  "timestamp": "2025-05-14T10:00:30.000Z",
  "data": {
    "screenshot": {
      "_id": "67b8a1c2d3e4f5a6b7c8d9e1",
      "url": "https://example.com",
      "status": "failed"
    },
    "user": {
      "id": "67a1b2c3d4e5f6a7b8c9d0e1"
    },
    "error": "Page navigation timed out."
  }
}
```

## Webhook headers

Each webhook delivery includes these headers:

| Header                | Description                                   |
| --------------------- | --------------------------------------------- |
| `Content-Type`        | `application/json`                            |
| `User-Agent`          | `Screenshotly-Webhooks/1.0`                   |
| `X-Webhook-Signature` | HMAC-SHA256 signature for verification        |
| `X-Webhook-Event`     | The event type (e.g., `screenshot.completed`) |
| `X-Webhook-ID`        | Your webhook configuration ID                 |
| `X-Webhook-Timestamp` | ISO 8601 timestamp of the event               |

## Signature verification

Every webhook delivery is signed using your webhook's secret key with HMAC-SHA256. Verify the signature to ensure the request is from Screenshotly:

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
```

## Handling webhooks

Here's a complete Express.js handler with signature verification:

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.SCREENSHOTLY_WEBHOOK_SECRET;

app.post('/webhook/screenshots', (req, res) => {
  // Verify signature
  const signature = req.headers['x-webhook-signature'];
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expectedSignature) {
    return res.status(401).send('Invalid signature');
  }

  const { event, event_id, data } = req.body;

  switch (event) {
    case 'screenshot.completed':
      console.log(`Screenshot ready: ${data.screenshot.imageUrl}`);
      break;
    case 'screenshot.failed':
      console.error(`Screenshot failed: ${data.error}`);
      break;
    case 'screenshot.deleted':
      console.log(`Screenshot deleted: ${data.screenshot._id}`);
      break;
  }

  // Respond quickly with 200 to acknowledge receipt
  res.status(200).send('OK');
});
```

## Retry behavior

If your webhook endpoint returns a non-2xx status code or times out (10 second limit), Screenshotly retries delivery with exponential backoff. You can configure retry policies in the dashboard:

* **Max retries**: 0–10 attempts (default: 3)
* **Retry delay**: 100–60,000ms (default: 1,000ms)
* **Backoff multiplier**: 1–5x (default: 2x)

Delivery logs are retained for 90 days.

## Best practices

* **Verify signatures** — always validate the `X-Webhook-Signature` header to ensure requests are from Screenshotly
* **Respond quickly** — return a `200` response immediately, then process the payload asynchronously
* **Handle duplicates** — use the `event_id` field for deduplication, as the same event may be delivered more than once
* **Use HTTPS** — always use an HTTPS endpoint for your webhook URL
* **Monitor delivery logs** — check your webhook's delivery history in the dashboard to debug failures
