# Webhooks
> How to receive real-time notifications about your documents' events, and how to verify they come from Legaldoc.
Webhooks are HTTP notifications Legaldoc.io sends to a URL of your choice whenever an event occurs on a document or template — without you having to poll the API periodically to find out if something changed.
Typical use cases: syncing a document's status with your database, triggering an automated workflow when a signature completes, or integrating Legaldoc with your CRM or other third-party systems.
## How they work
1. You configure a webhook URL in Legaldoc.
2. When an event occurs, Legaldoc sends a `POST` to that URL with the event type and the document's data.
3. Your server processes the event and responds `200 OK`.
## Available events
### Document events
| Event | Fires when... |
|---|---|
| `DOCUMENT_CREATED` | A new document is created. |
| `DOCUMENT_SENT` | The document is sent to its recipients. |
| `DOCUMENT_OPENED` | A recipient opens the document for the first time. |
| `DOCUMENT_SIGNED` | A recipient signs. Fires for each individual signature, not just on completion. |
| `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action (signing, approving, or viewing). |
| `DOCUMENT_COMPLETED` | All recipients have completed their action. |
| `DOCUMENT_REJECTED` | A recipient rejects the document. |
| `DOCUMENT_CANCELLED` | The document owner cancels or deletes it. |
| `DOCUMENT_REMINDER_SENT` | A reminder is sent to a pending recipient. |
### Template events
| Event | Fires when... |
|---|---|
| `TEMPLATE_CREATED` | A new template is created. |
| `TEMPLATE_UPDATED` | A template is modified (settings, recipients, or fields). |
| `TEMPLATE_DELETED` | A template is deleted. |
| `TEMPLATE_USED` | A document is created from a template — fires alongside `DOCUMENT_CREATED`. |
For a standard signing flow, the events you'll almost always care about are `DOCUMENT_COMPLETED` and `DOCUMENT_REJECTED` — see [Integration Guide](/en/guides/integration-guide/#5-knowing-when-its-done). The rest are useful for finer-grained tracking, for example notifying an internal user when a specific recipient signs within a sequential, multi-signer flow.
You can subscribe to all events or only the ones you need.
## Payload structure
Every notification shares this shape:
```json
{
"event": "DOCUMENT_COMPLETED",
"payload": { /* document or template, with its recipients */ },
"createdAt": "2024-04-22T11:52:18.277Z",
"webhookEndpoint": "https://your-server.com/webhooks/legaldoc"
}
```
| Field | Description |
|---|---|
| `event` | The event type identifier — one of the ones listed above. |
| `payload` | The affected document or template, including its list of recipients and each one's current status. |
| `createdAt` | When this notification was generated. |
| `webhookEndpoint` | The URL this notification is being delivered to. |
Inside `payload`, each recipient carries its own status: `signingStatus` (`NOT_SIGNED`, `SIGNED`, `REJECTED`), `readStatus` (`NOT_OPENED`, `OPENED`), and, if rejected, `rejectionReason`. The full detail of each resource is in the [API Reference](/en/api/).
## Setting up a webhook
From your account or team settings, in the Webhooks section:
1. Provide the **URL** that will receive notifications (must be HTTPS).
2. Choose which **events** you want to subscribe to.
3. Optionally set a **secret** — you'll need it to verify the authenticity of each notification (see below).
Your endpoint must meet these requirements:
| Requirement | Detail |
|---|---|
| Protocol | HTTPS |
| Method | Accepts `POST` |
| Content-Type | `application/json` |
| Response | `2xx` within 30 seconds |
| Availability | Publicly reachable from the internet |
For local development, expose your server through a tunnel (for example [ngrok](https://ngrok.com)) so you can receive real notifications while testing.
## Verifying authenticity
If you configured a secret, every notification includes the `X-Legaldoc-Secret` header carrying that value:
```http
POST /webhooks/legaldoc HTTP/1.1
Content-Type: application/json
X-Legaldoc-Secret: your_configured_secret
{"event": "DOCUMENT_COMPLETED", "payload": { /* ... */ }}
```
Before processing any notification, compare that header against your stored secret using a constant-time comparison (not `===` or `==`), so you don't leak information about the secret through response-time variations:
```javascript
const crypto = require('crypto');
function isValid(receivedSecret, expectedSecret) {
if (!expectedSecret) return true; // no secret configured
if (!receivedSecret) return false;
try {
return crypto.timingSafeEqual(
Buffer.from(receivedSecret),
Buffer.from(expectedSecret),
);
} catch {
return false; // different lengths
}
}
app.post('/webhooks/legaldoc', (req, res) => {
const secret = req.headers['x-legaldoc-secret'];
if (!isValid(secret, process.env.LEGALDOC_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, payload } = req.body;
// process the event...
res.status(200).json({ received: true });
});
```
If verification fails, respond with `401` without detailing why, and log the attempt for monitoring — never process the payload of a notification that didn't verify.
## Retries
If your endpoint doesn't respond `2xx` in time, Legaldoc retries delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After the fifth failed attempt, the notification is marked as failed and isn't retried automatically. That's why your handler should:
- **Respond fast**: acknowledge with `200 OK` immediately and process the event asynchronously, instead of doing all the work inside the same request.
- **Process idempotently**: the same notification can arrive more than once (retries, or a manual resend) — processing it twice shouldn't cause duplicate effects in your system.
## Availability
Webhooks are available for individual users and teams.