Webhooks
Instead of polling, register an endpoint and we push events to it as they happen — an envelope is completed, a participant signs, a document is ready. Every delivery is signed so you can trust it.
Registering an endpoint
Create a webhook endpoint from the dashboard or via the API with the webhooks:manage scope. You choose which events to subscribe to (an empty list means all events). In
production the URL must be HTTPS. On creation we return a signing secret — shown only once.
Plan feature
403 plan_feature_required:webhooks.Event payload
Every event is delivered as a POST with a JSON body:
{
"id": "evt_9f2c...",
"type": "envelope.completed",
"created_at": "2026-07-21T14:03:22Z",
"data": {
"envelope": {
"id": "6b1d...",
"status": "completed"
}
}
}Event catalog
| Event | When it fires |
|---|---|
document.created | A document finished processing and is ready. |
document.quarantined | An uploaded file failed validation. |
envelope.sent | An envelope was sent to its participants. |
envelope.completed | Every required participant has signed. |
envelope.cancelled | The envelope was cancelled. |
envelope.expired | The envelope reached its expiry date. |
envelope.declined | A participant declined, ending the envelope. |
participant.invited | A participant was invited to sign. |
participant.viewed | A participant opened the signing page. |
participant.signed | A participant completed their signature. |
participant.declined | A participant declined to sign. |
participant.delegated | A participant handed their turn over to someone else (envelopes with allow_delegation). |
credits.low | Your credit balance crossed the low threshold. |
payment.completed | A payment was confirmed. |
webhook.test | A test delivery you trigger from the dashboard. |
Delivery headers
Each delivery carries three headers you'll use to verify and de-duplicate it:
X-Webhook-Id: d4e5f6... # unique per delivery — dedupe on this
X-Webhook-Timestamp: 1753106602
X-Webhook-Signature: 9a8b7c6d... # HMAC-SHA256 hexVerifying the signature
The signature is HMAC-SHA256 over the string {timestamp}.{raw_body} using your endpoint secret. Recompute it from the raw request body (before any JSON parsing), compare in constant time against X-Webhook-Signature, and reject deliveries whose timestamp is more than 5 minutes old.
import crypto from 'node:crypto';
const TOLERANCE = 300; // seconds
function verify(rawBody, headers, secret) {
const ts = Number(headers['x-webhook-timestamp']);
if (Math.abs(Date.now() / 1000 - ts) > TOLERANCE) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
const got = headers['x-webhook-signature'];
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}Use the raw body
Retries & idempotency
A delivery is considered successful on a 2xx response. Failed deliveries are retried
with backoff, so your endpoint may receive the same event more than once — use X-Webhook-Id to make processing idempotent. You can resend past deliveries or send a webhook.test event from the dashboard.