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

Outbound webhooks require the Growth plan or above. On a plan without them, creating an endpoint returns 403 plan_feature_required:webhooks.

Event payload

Every event is delivered as a POST with a JSON body:

POST your-endpoint · body
{
  "id": "evt_9f2c...",
  "type": "envelope.completed",
  "created_at": "2026-07-21T14:03:22Z",
  "data": {
    "envelope": {
      "id": "6b1d...",
      "status": "completed"
    }
  }
}

Event catalog

EventWhen it fires
document.createdA document finished processing and is ready.
document.quarantinedAn uploaded file failed validation.
envelope.sentAn envelope was sent to its participants.
envelope.completedEvery required participant has signed.
envelope.cancelledThe envelope was cancelled.
envelope.expiredThe envelope reached its expiry date.
envelope.declinedA participant declined, ending the envelope.
participant.invitedA participant was invited to sign.
participant.viewedA participant opened the signing page.
participant.signedA participant completed their signature.
participant.declinedA participant declined to sign.
participant.delegatedA participant handed their turn over to someone else (envelopes with allow_delegation).
credits.lowYour credit balance crossed the low threshold.
payment.completedA payment was confirmed.
webhook.testA test delivery you trigger from the dashboard.

Delivery headers

Each delivery carries three headers you'll use to verify and de-duplicate it:

request headers
X-Webhook-Id:        d4e5f6...          # unique per delivery — dedupe on this
X-Webhook-Timestamp: 1753106602
X-Webhook-Signature: 9a8b7c6d...          # HMAC-SHA256 hex

Verifying 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

Compute the HMAC over the exact bytes you received. Re-serializing the parsed JSON can change whitespace or key order and will break the signature.

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.