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

# Webhooks

> Receive real-time notifications when async operations complete.

## Overview

Webhooks notify your server when asynchronous operations complete — batch jobs finishing, large renders completing, or async conversions wrapping up.

All webhook payloads are signed with HMAC-SHA256 so you can verify they came from PDFBase.

## Setup

Configure webhook endpoints in your [dashboard](https://app.pdfbase.dev/webhooks) or via the API:

```bash theme={null}
curl -X POST https://api.pdfbase.dev/v1/webhook_endpoints \
  -H "Authorization: Bearer pk_live_xxx" \
  -d '{
    "url": "https://your-app.com/webhooks/pdfbase",
    "events": ["pdf.completed", "batch.completed", "batch.failed"]
  }'
```

```json theme={null}
{
  "id": "we_abc123",
  "object": "webhook_endpoint",
  "url": "https://your-app.com/webhooks/pdfbase",
  "events": ["pdf.completed", "batch.completed", "batch.failed"],
  "secret": "whsec_abc123def456",
  "status": "active",
  "created_at": "2026-05-19T10:00:00Z"
}
```

<Warning>
  Store the `secret` securely. It's only returned once at creation time. You'll need it to verify webhook signatures.
</Warning>

## Webhook payload

```json theme={null}
{
  "id": "evt_xyz789",
  "object": "event",
  "type": "pdf.completed",
  "created_at": "2026-05-19T10:30:05Z",
  "data": {
    "id": "pdf_x7Kf9m",
    "object": "pdf",
    "status": "completed",
    "url": "https://files.pdfbase.dev/pdf_x7Kf9m.pdf?token=sig_abc",
    "pages": 3,
    "bytes": 142000
  }
}
```

## Verifying signatures

Every webhook request includes a `PDFBase-Signature` header:

```
PDFBase-Signature: t=1716120005,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

Verify it by computing HMAC-SHA256 of `{timestamp}.{raw_body}` using your webhook secret:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import crypto from 'crypto'

  function verifyWebhook(payload: string, header: string, secret: string): boolean {
    const [tPart, sigPart] = header.split(',')
    const timestamp = tPart.split('=')[1]
    const signature = sigPart.split('=')[1]

    // Reject if timestamp is more than 5 minutes old
    const age = Math.floor(Date.now() / 1000) - parseInt(timestamp)
    if (age > 300) return false

    const expected = crypto
      .createHmac('sha256', secret)
      .update(`${timestamp}.${payload}`)
      .digest('hex')

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

  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  def verify_webhook(payload: str, header: str, secret: str) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      timestamp = parts["t"]
      signature = parts["v1"]

      # Reject if timestamp is more than 5 minutes old
      if int(time.time()) - int(timestamp) > 300:
          return False

      expected = hmac.new(
          secret.encode(),
          f"{timestamp}.{payload}".encode(),
          hashlib.sha256,
      ).hexdigest()

      return hmac.compare_digest(signature, expected)
  ```
</CodeGroup>

## Event types

### Phase 1 (Launch)

| Event                  | Fires when                                           |
| ---------------------- | ---------------------------------------------------- |
| `pdf.completed`        | A PDF generation finishes successfully               |
| `pdf.failed`           | A PDF generation fails                               |
| `batch.completed`      | All items in a batch job finish                      |
| `batch.failed`         | A batch job fails (partial results may be available) |
| `batch.item.completed` | A single item within a batch finishes                |
| `batch.item.failed`    | A single item within a batch fails                   |

### Phase 2

| Event               | Fires when                                          |
| ------------------- | --------------------------------------------------- |
| `edit.completed`    | A merge/split/watermark/compress operation finishes |
| `convert.completed` | A format conversion finishes                        |

### Phase 3

| Event               | Fires when                           |
| ------------------- | ------------------------------------ |
| `extract.completed` | A text/OCR/table extraction finishes |

## Retry behavior

Failed webhook deliveries are retried with exponential backoff:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 1 minute   |
| 2nd retry | 5 minutes  |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours    |
| 5th retry | 12 hours   |

After 5 failed attempts, the event is marked as `failed` and visible in your dashboard. You can manually retry from there.

A delivery is considered failed if your endpoint:

* Returns a non-2xx status code
* Doesn't respond within 15 seconds
* Has a connection error

## Testing webhooks

Use the dashboard's **Send test event** button to fire a sample payload to your endpoint. Or use the CLI:

```bash theme={null}
pdfbase webhooks test we_abc123 --event pdf.completed
```
