Developers

Webhooks

Receive real-time signing events over HTTP with HMAC-signed deliveries, automatic retries, and a replay-from-console safety net.

HMAC

SHA-256 request signing

Per-webhook secret, sent as X-Endorsed-Signature

10s

Delivery timeout

Return 2xx within the window or the attempt fails

5×

Retries after the first attempt

60s, 5m, 15m, 1h, then 6h

Replay

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 typeWhen it firesKey `data` fields
document.createdYou upload a document via the APIdocumentId, customerId, externalDocumentId
document.sentSigning link dispatched to first recipientdocumentId, customerId, channel
document.viewedSigner opens the signing linkdocumentId, signerEmail, viewedAt
document.signedIndividual signer completes their stepdocumentId, signerEmail, signedAt
document.completedAll parties have signeddocumentId, completedAt, downloadUrl
document.declinedA signer explicitly declinesdocumentId, signerEmail, reason
document.expiredDocument reaches its expiry date unsigneddocumentId, expiredAt
signing.startedSigning-session token is redeemeddocumentId, sessionId, signerEmail
signing.completedSigning session finishes (signed or declined)documentId, sessionId, outcome
customer.created / .updated / .deletedYou mutate a customer recordcustomerId, externalCustomerId
template.created / .updatedYou mutate a templatetemplateId, 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.

typescript
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. 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. 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. 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. 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.

Delivery event history with document lifecycle entries
Delivery event history — signed lifecycle events with timestamps and status outcomes for operational validation.

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.