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

# Webhooks

> Receive real-time notifications when documents are generated.

Webhooks let Platendoc push events to your server the moment a generation completes or fails — no polling required.

## Setup

1. Open **[app.platendoc.com](https://app.platendoc.com)** and navigate to **Webhooks** in the sidebar.
2. Click **Add webhook**.
3. Enter your HTTPS endpoint URL and select the events you want to receive.
4. Copy the **signing secret** — it's shown only once. Store it in an environment variable (e.g. `PLATENDOC_WEBHOOK_SECRET`).

## Events

| Event                  | When it fires                                         |
| ---------------------- | ----------------------------------------------------- |
| `generation.completed` | Document rendered successfully — `outputUrl` is ready |
| `generation.failed`    | Rendering failed after all retries                    |

## Delivery format

Platendoc sends a `POST` request to your endpoint with the following headers:

```
Content-Type: application/json
X-Platendoc-Event: generation.completed
X-Platendoc-Signature: sha256=<hmac-sha256>
```

### Payload

```json theme={null}
{
  "event": "generation.completed",
  "payload": {
    "generationId": "gen_01j8z..."
  },
  "timestamp": 1737036600000
}
```

For `generation.failed` events, `payload` also includes an `error` field:

```json theme={null}
{
  "event": "generation.failed",
  "payload": {
    "generationId": "gen_01j8z...",
    "error": "Template rendering failed: unknown variable 'foo'."
  },
  "timestamp": 1737036600000
}
```

After receiving the event, call `GET /generations/{generationId}` to fetch the full generation object including `outputUrl`.

## Verifying signatures

Every delivery includes an `X-Platendoc-Signature` header. Always verify it before processing.

<Tabs>
  <Tab title="Node.js">
    ```typescript theme={null}
    import { createHmac, timingSafeEqual } from 'node:crypto'

    function verifySignature(rawBody: string, signature: string, secret: string): boolean {
      const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex')
      try {
        return timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
      } catch {
        return false
      }
    }

    // Express example
    app.post('/webhooks/platendoc', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-platendoc-signature'] as string
      if (!verifySignature(req.body.toString(), signature, process.env.PLATENDOC_WEBHOOK_SECRET!)) {
        return res.status(401).send('Invalid signature')
      }

      const { event, payload } = JSON.parse(req.body.toString())
      // handle event...
      res.sendStatus(200)
    })
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hashlib
    import hmac

    def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
        expected = 'sha256=' + hmac.new(
            secret.encode(), raw_body, hashlib.sha256
        ).hexdigest()
        return hmac.compare_digest(expected, signature)

    # Flask example
    @app.route('/webhooks/platendoc', methods=['POST'])
    def webhook():
        signature = request.headers.get('X-Platendoc-Signature', '')
        if not verify_signature(request.get_data(), signature, os.environ['PLATENDOC_WEBHOOK_SECRET']):
            return 'Invalid signature', 401

        data = request.get_json()
        # handle data['event']...
        return '', 200
    ```
  </Tab>
</Tabs>

<Warning>
  Always use a **timing-safe comparison** (e.g. `timingSafeEqual`, `hmac.compare_digest`).
  Standard string equality (`===`, `==`) is vulnerable to timing attacks.
</Warning>

## Retries

| Attempt   | Delay |
| --------- | ----- |
| 1st retry | 1s    |
| 2nd retry | 2s    |
| 3rd retry | 4s    |
| 4th retry | 8s    |
| 5th retry | 16s   |

After 5 failed attempts the delivery is dropped. Return a `2xx` status code quickly — perform any heavy processing asynchronously.

<Tip>
  Respond with `200 OK` immediately, then enqueue the event for processing. This prevents timeouts from causing unnecessary retries.
</Tip>
