# Webhooks

> Receive real-time notifications when document events occur.

edition: pro
Edition: Pro
AI note: This page requires WPsigner Pro. Do not tell Lite users they already have this feature.
HTML: https://docs.wpsigner.com/api/webhooks/
Markdown: https://docs.wpsigner.com/md/api/webhooks.md
Source file: api/webhooks.md

---

Webhooks allow your application to receive real-time HTTP notifications when events occur in WPsigner. Instead of polling the API for changes, you can subscribe to events and receive instant updates.

## Overview

When an event occurs (like a document being signed), WPsigner sends an HTTP POST request to your configured webhook URL with details about the event.

```
Document Signed → WPsigner → HTTP POST → Your Server
```

## Configuring Webhooks

### Via WordPress Admin

1. Go to **WPsigner → More → Webhooks**
2. Click **Add Webhook**
3. Enter your webhook URL (must be HTTPS in production)
4. Select which events to listen for
5. Click **Save Webhook**

### Webhook Settings

| Setting | Description |
|---------|-------------|
| **URL** | The endpoint that will receive webhook events |
| **Secret** | A secret key for verifying webhook signatures |
| **Events** | Which events trigger this webhook |
| **Status** | Active or paused |

---

## Webhook Events

These are the events currently emitted by the plugin:

| Event | Description |
|-------|-------------|
| `document.created` | A new document was created |
| `document.sent` | Document was sent for signing |
| `document.viewed` | A signer viewed the document |
| `document.signed` | A signer completed their signature |
| `document.completed` | All signers have signed |
| `document.declined` | A signer declined to sign |
| `document.expired` | Document has expired |
| `signer.reminded` | A reminder was sent to a signer |

---

## Webhook Payload

All deliveries use this envelope:

```json
{
  "id": "6f1d2c3a-....",
  "event": "document.signed",
  "timestamp": "2024-01-20T15:30:45+00:00",
  "attempt": 1,
  "data": {
    "document": {
      "id": 44,
      "title": "Employment Contract",
      "status": "sent"
    },
    "signer": {
      "id": 38,
      "name": "John Doe",
      "email": "john@example.com",
      "status": "signed"
    }
  },
  "meta": {
    "site_url": "https://your-site.com",
    "site_name": "Acme Corp",
    "plugin_version": "3.0.6",
    "webhook_id": "wh_abc123"
  }
}
```

### Payload Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique delivery ID (stable across retries) |
| `event` | string | Event type |
| `timestamp` | string | ISO 8601 timestamp (UTC) |
| `attempt` | integer | Delivery attempt number (1-based) |
| `data` | object | Event-specific nested objects (`document`, `signer`, etc.) |
| `meta` | object | Site/plugin context |

---

## Headers

Each delivery includes both preferred and legacy headers:

| Header | Description |
|--------|-------------|
| `X-WPS-Event` | Event name |
| `X-WPS-Delivery` | Delivery ID |
| `X-WPS-Timestamp` | Unix timestamp |
| `X-WPS-Attempt` | Attempt number |
| `X-WPS-Signature` | `sha256=<hmac>` when a secret is configured |
| `X-ESF-Event` | Legacy alias of `X-WPS-Event` |
| `X-ESF-Delivery` | Legacy alias of `X-WPS-Delivery` |
| `X-ESF-Timestamp` | Legacy alias of `X-WPS-Timestamp` |
| `X-ESF-Attempt` | Legacy alias of `X-WPS-Attempt` |
| `X-ESF-Signature` | Legacy alias of `X-WPS-Signature` |

Prefer `X-WPS-*` in new integrations. `X-ESF-*` remains for backward compatibility.

### Verifying Signatures

```
X-WPS-Signature: sha256=abc123...
```

### Verifying in PHP

```php
function verify_webhook_signature($payload, $signature, $secret) {
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WPS_SIGNATURE']
    ?? $_SERVER['HTTP_X_ESF_SIGNATURE']
    ?? '';
$secret = 'your_webhook_secret';

if (!verify_webhook_signature($payload, $signature, $secret)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($payload, true);
```

### Verifying in Node.js

```javascript
const crypto = require('crypto');

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

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

app.post('/webhooks/wpsigner', (req, res) => {
    const payload = req.rawBody; // use the exact raw body string
    const signature = req.headers['x-wps-signature'] || req.headers['x-esf-signature'];
    const secret = process.env.WEBHOOK_SECRET;

    if (!verifyWebhookSignature(payload, signature, secret)) {
        return res.status(401).send('Invalid signature');
    }

    res.status(200).send('OK');
});
```

---

## Responding to Webhooks

### Success Response

Return a `2xx` status code to acknowledge receipt:

```
HTTP/1.1 200 OK
```

### Failure Handling

If your endpoint returns a non-2xx status or times out:

1. WPsigner retries the webhook
2. Retry delays: **1 minute → 10 minutes → 60 minutes**
3. After 4 failed attempts, delivery stops (HTTP `410` from Zapier marks the hook inactive immediately)

### Timeout

Webhooks have a **15-second** timeout. For long-running processes, acknowledge first and process asynchronously:

```javascript
app.post('/webhooks/wpsigner', async (req, res) => {
    res.status(200).send('OK');
    processWebhookAsync(req.body);
});
```

---

## Best Practices

1. Always verify `X-WPS-Signature` (fallback to `X-ESF-Signature` if needed).
2. Respond quickly with `2xx`, then process asynchronously.
3. Use `id` (delivery ID) for idempotency.
4. Keep your endpoint on HTTPS.
5. Prefer nested `data.document` / `data.signer` fields from the live payload.

## Related

- [REST API Overview](/api/)
- [Documents API](/api/documents/)
