Webhooks
Receive real-time signing events over HTTP with HMAC-signed deliveries, automatic retries, and a replay-from-console safety net.
SHA-256 request signing
Per-webhook secret, sent as X-Endorsed-Signature
Delivery timeout
Return 2xx within the window or the attempt fails
Retries after the first attempt
60s, 5m, 15m, 1h, then 6h
From the developer console
Inspect every attempt and replay individual events
What you can do with webhook events
Every significant lifecycle transition emits a typed event. Use them to update your own records without polling.
Trigger downstream actions
When an envelope is fully signed, update your CRM, release a provisioned resource, or send a confirmation inside your own product — all without polling the API.
Sync document status
Keep your database in sync with signing lifecycle: sent, opened, signed, declined, expired, voided. One event per transition, no polling loop required.
Audit and compliance logging
Feed webhook events into your own audit store. Correlation IDs link every event back to the originating API request for full traceability.
Replay failed deliveries
Events that exhaust the retry ladder land in a dead-letter queue you can inspect in the developer console and replay intentionally.
Webhook event reference
Register a webhook, pick which events you want, and EndorsedSign delivers a JSON payload with `event`, `tenantId`, `timestamp`, and `data`. Event names match the strings you send in the webhook create call.
| Event type | When it fires | Key `data` fields |
|---|---|---|
| document.created | You upload a document via the API | documentId, customerId, externalDocumentId |
| document.sent | Signing link dispatched to first recipient | documentId, customerId, channel |
| document.viewed | Signer opens the signing link | documentId, signerEmail, viewedAt |
| document.signed | Individual signer completes their step | documentId, signerEmail, signedAt |
| document.completed | All parties have signed | documentId, completedAt, downloadUrl |
| document.declined | A signer explicitly declines | documentId, signerEmail, reason |
| document.expired | Document reaches its expiry date unsigned | documentId, expiredAt |
| signing.started | Signing-session token is redeemed | documentId, sessionId, signerEmail |
| signing.completed | Signing session finishes (signed or declined) | documentId, sessionId, outcome |
| customer.created / .updated / .deleted | You mutate a customer record | customerId, externalCustomerId |
| template.created / .updated | You mutate a template | templateId, name |
Full list lives in backend `WEBHOOK_EVENTS`. Add or narrow subscriptions per webhook — the delivery pipeline fans out one HTTP call per registered endpoint.
Verifying webhook signatures (Node.js)
Every delivery includes an `X-Endorsed-Signature` header: `t={unix_seconds},v1={hex}`. `v1` is HMAC-SHA256 of `{timestamp}.{rawBody}` using the secret returned when the webhook was created. Reject timestamps outside ±5 minutes. `X-Webhook-Signature` may still appear on developer-portal test deliveries as a deprecated body-only alias — verify `X-Endorsed-Signature`. Prerequisites: your webhook secret in an env var, an Express route that keeps the raw body. Expected output: unrecognised or tampered payloads are rejected with 401 before any handler runs.
import crypto from 'node:crypto';
import express from 'express';
const WEBHOOK_SECRET = process.env.ENDORSED_WEBHOOK_SECRET!;
const TOLERANCE_SECONDS = 5 * 60;
export function verifyWebhookSignature(
rawBody: string,
header: string | undefined,
nowSeconds = Math.floor(Date.now() / 1000),
): boolean {
if (!header) return false;
let timestamp: number | null = null;
let v1 = '';
for (const part of header.split(',')) {
const eq = part.indexOf('=');
if (eq === -1) continue;
const key = part.slice(0, eq).trim();
const value = part.slice(eq + 1).trim();
if (key === 't' && /^\d+$/.test(value)) timestamp = Number.parseInt(value, 10);
if (key === 'v1' && /^[a-f0-9]{64}$/i.test(value)) v1 = value.toLowerCase();
}
if (timestamp === null || !v1) return false;
if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(timestamp + '.' + rawBody)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(v1, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post(
'/webhooks/endorsed',
express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf.toString('utf8'); } }),
(req, res) => {
const sig = req.header('X-Endorsed-Signature');
if (!verifyWebhookSignature((req as any).rawBody, sig)) {
return res.status(401).send('Invalid signature');
}
res.sendStatus(200);
},
);Return 2xx within 30s or the delivery is marked failed. Failed attempts retry with the ladder above and remain replayable from the developer console.
How to set up a webhook endpoint
- 1
Create the endpoint in your app
Add a route that accepts POST requests and returns HTTP 200 within 10 seconds. Any 2xx response counts as successful delivery.
- 2
Register the endpoint in your tenant settings
Go to Admin → Settings → Webhooks. Enter your endpoint URL and select the event types you want. A signing secret is generated per endpoint.
- 3
Verify the signature on every request
Read `X-Endorsed-Signature`. Recompute HMAC-SHA256 over `{timestamp}.{rawBody}` and constant-time compare with `v1` (see the code sample above). Reject a missing header, a bad hex value, a timestamp outside ±5 minutes, or an HMAC mismatch. `X-Webhook-Signature` is a deprecated alias on some test deliveries — do not treat it as the live contract.
- 4
Return 2xx quickly, process async
Acknowledge delivery with 200 within 10 seconds and enqueue the event body for async processing. The developer console shows every attempt (headers, body, status, timing) and lets you replay any delivery — no need to re-trigger the source signing flow just to test your handler.
Common questions
A real webhook payload — document completed
When all parties have signed, EndorsedSign POSTs this payload to your registered endpoint. Verify the HMAC signature, enqueue, and acknowledge with 200.

Wire up your first webhook
Claim a sandbox, register the endpoint in Admin → Developer → Webhooks, and inspect or replay each delivery from the developer console.