Signing session webhooks keep your application up to date without polling. Webhooks are session-specific: add up to two endpoints when you create a signing session, and each endpoint receives every event for that session.
{
"webhooks": [
{
"url": "https://example.com/webhooks/signing",
"secret": "your-shared-secret"
}
]
}We recommend configuring a secret and validating every delivery.
Docue sends each event as an HTTP POST with:
Content-Type: application/jsonX-Webhook-Id: identifies this endpoint delivery and remains the same across retriesX-Webhook-Sent-At: UTC time (ISO 8601, e.g.2026-02-12T10:31:00.000Z) when this delivery attempt was sent. It is refreshed on every retry, while the request body stays identicalX-Webhook-Signature: included when a secret is configured
Return any 2xx response within five seconds to acknowledge the delivery. Acknowledge quickly and do any heavy processing asynchronously on your side. Timeouts, network errors, and non-2xx responses are treated as failures and retried. Deliveries can therefore arrive more than once or out of order. Make processing idempotent using event.meta.id, which identifies the business event independently of its endpoint delivery.
Failed deliveries are retried with an increasing backoff, up to 8 attempts in total (the initial delivery plus 7 retries). The wait before each retry is:
| After failed attempt | Wait before next attempt |
|---|---|
| 1 | 5 seconds |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 5 hours |
| 6 | 10 hours |
| 7 | 10 hours |
If the 8th attempt also fails, the delivery is marked exhausted and no further retries are made.
Use List recent webhook delivery attempts to inspect delivery status (pending, delivered, or exhausted), attempt count, retry_after, and last_error for a session.
The body contains the event and resource snapshots as they were when the event occurred:
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:00.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "signed",
"status_changed_at": "2026-02-12T10:31:00.000Z",
"decline_reason": null
}
},
"meta": {
"type": "com.docue.sign.v1.participant.signed",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88411",
"timestamp": "2026-02-12T10:31:00.000Z"
}
}
}Use event.meta.type to route the event. Every payload contains workspace and session; the other fields depend on the event type. Session snapshots contain id, external_id, status, and status_changed_at. Participant snapshots contain id, external_id, session_id, status, status_changed_at, and decline_reason. Invitation snapshots contain id, participant_id, and a timeline with created_at, sent_at, opened_at, and revoked_at. external_id and decline_reason are null when unset.
Expand an event type to see a full webhook request body. Status values below are representative of when each event is typically delivered; always read the live status and status_changed_at fields from the payload.
com.docue.sign.v1.session.created — The session is created.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z"
},
"participants": [
{
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z",
"decline_reason": null
}
],
"invitations": [
{
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88311",
"participant_id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"timeline": {
"created_at": "2026-02-12T10:30:00.000Z",
"sent_at": null,
"opened_at": null,
"revoked_at": null
}
}
]
},
"meta": {
"type": "com.docue.sign.v1.session.created",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88411",
"timestamp": "2026-02-12T10:30:00.000Z"
}
}
}com.docue.sign.v1.session.started — The session is started.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
}
},
"meta": {
"type": "com.docue.sign.v1.session.started",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88412",
"timestamp": "2026-02-12T10:30:05.000Z"
}
}
}com.docue.sign.v1.session.cancelled — The session is cancelled.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "cancelled",
"status_changed_at": "2026-02-12T11:00:00.000Z"
}
},
"meta": {
"type": "com.docue.sign.v1.session.cancelled",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88415",
"timestamp": "2026-02-12T11:00:00.000Z"
}
}
}com.docue.sign.v1.session.discarded — The session is discarded.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "discarded",
"status_changed_at": "2026-02-12T11:00:00.000Z"
}
},
"meta": {
"type": "com.docue.sign.v1.session.discarded",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88413",
"timestamp": "2026-02-12T11:00:00.000Z"
}
}
}com.docue.sign.v1.session.completed — Every participant has signed.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "completed",
"status_changed_at": "2026-02-12T10:45:00.000Z"
}
},
"meta": {
"type": "com.docue.sign.v1.session.completed",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88414",
"timestamp": "2026-02-12T10:45:00.000Z"
}
}
}com.docue.sign.v1.session.expired — The session expired before it was completed.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "expired",
"status_changed_at": "2026-02-13T08:00:00.000Z"
}
},
"meta": {
"type": "com.docue.sign.v1.session.expired",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88415",
"timestamp": "2026-02-13T08:00:00.000Z"
}
}
}com.docue.sign.v1.session.renewed — An expired session is renewed with a new due date.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-13T09:00:00.000Z"
}
},
"meta": {
"type": "com.docue.sign.v1.session.renewed",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88416",
"timestamp": "2026-02-13T09:00:00.000Z"
}
}
}com.docue.sign.v1.participant.added — A participant is added.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88212",
"external_id": "user-124",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:10.000Z",
"decline_reason": null
},
"invitations": [
{
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88321",
"participant_id": "0196ae5b-2e9d-729f-aba5-fa4db6f88212",
"timeline": {
"created_at": "2026-02-12T10:30:10.000Z",
"sent_at": null,
"opened_at": null,
"revoked_at": null
}
}
]
},
"meta": {
"type": "com.docue.sign.v1.participant.added",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88415",
"timestamp": "2026-02-12T10:30:10.000Z"
}
}
}com.docue.sign.v1.participant.removed — A participant is removed.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88212",
"external_id": "user-124",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:10.000Z",
"decline_reason": null
}
},
"meta": {
"type": "com.docue.sign.v1.participant.removed",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88416",
"timestamp": "2026-02-12T10:30:20.000Z"
}
}
}com.docue.sign.v1.participant.signed — A participant signs.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "signed",
"status_changed_at": "2026-02-12T10:36:00.000Z",
"decline_reason": null
}
},
"meta": {
"type": "com.docue.sign.v1.participant.signed",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88418",
"timestamp": "2026-02-12T10:36:00.000Z"
}
}
}com.docue.sign.v1.participant.declined — A participant declines to sign.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "declined",
"status_changed_at": "2026-02-12T10:38:00.000Z",
"decline_reason": "I do not agree with the terms."
}
},
"meta": {
"type": "com.docue.sign.v1.participant.declined",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88417",
"timestamp": "2026-02-12T10:38:00.000Z"
}
}
}com.docue.sign.v1.participant.excused — A participant is excused from signing.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:40:00.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "excused",
"status_changed_at": "2026-02-12T10:42:00.000Z",
"decline_reason": null
}
},
"meta": {
"type": "com.docue.sign.v1.participant.excused",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88422",
"timestamp": "2026-02-12T10:42:00.000Z"
}
}
}com.docue.sign.v1.participant.reinstated — A declined participant is brought back to pending.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:40:00.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:45:00.000Z",
"decline_reason": null
}
},
"meta": {
"type": "com.docue.sign.v1.participant.reinstated",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88421",
"timestamp": "2026-02-12T10:45:00.000Z"
}
}
}com.docue.sign.v1.invitation.created — An invitation is created.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z",
"decline_reason": null
},
"invitation": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88311",
"participant_id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"timeline": {
"created_at": "2026-02-12T10:30:05.000Z",
"sent_at": null,
"opened_at": null,
"revoked_at": null
}
}
},
"meta": {
"type": "com.docue.sign.v1.invitation.created",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88419",
"timestamp": "2026-02-12T10:30:05.000Z"
}
}
}com.docue.sign.v1.invitation.sent — An invitation is sent by email or SMS. Resends fire invitation.resent instead.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z",
"decline_reason": null
},
"invitation": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88311",
"participant_id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"timeline": {
"created_at": "2026-02-12T10:30:05.000Z",
"sent_at": "2026-02-12T10:30:06.000Z",
"opened_at": null,
"revoked_at": null
}
}
},
"meta": {
"type": "com.docue.sign.v1.invitation.sent",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f8841c",
"timestamp": "2026-02-12T10:30:06.000Z"
}
}
}com.docue.sign.v1.invitation.resent — An invitation is sent again on request. The signing link stays the same.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z",
"decline_reason": null
},
"invitation": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88311",
"participant_id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"timeline": {
"created_at": "2026-02-12T10:30:05.000Z",
"sent_at": "2026-02-12T10:30:06.000Z",
"opened_at": null,
"revoked_at": null
}
}
},
"meta": {
"type": "com.docue.sign.v1.invitation.resent",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f8841d",
"timestamp": "2026-02-12T10:45:00.000Z"
}
}
}com.docue.sign.v1.invitation.opened — An invitation is opened.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "viewing",
"status_changed_at": "2026-02-12T10:32:00.000Z",
"decline_reason": null
},
"invitation": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88311",
"participant_id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"timeline": {
"created_at": "2026-02-12T10:30:05.000Z",
"sent_at": "2026-02-12T10:30:06.000Z",
"opened_at": "2026-02-12T10:32:00.000Z",
"revoked_at": null
}
}
},
"meta": {
"type": "com.docue.sign.v1.invitation.opened",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f8841a",
"timestamp": "2026-02-12T10:32:00.000Z"
}
}
}com.docue.sign.v1.invitation.revoked — An invitation is revoked.
{
"event": {
"payload": {
"workspace": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88001"
},
"session": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"external_id": "deal-42",
"status": "started",
"status_changed_at": "2026-02-12T10:30:05.000Z"
},
"participant": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"external_id": "user-123",
"session_id": "0196ae5b-2e9d-729f-aba5-fa4db6f881fd",
"status": "pending",
"status_changed_at": "2026-02-12T10:30:00.000Z",
"decline_reason": null
},
"invitation": {
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f88311",
"participant_id": "0196ae5b-2e9d-729f-aba5-fa4db6f88211",
"timeline": {
"created_at": "2026-02-12T10:30:05.000Z",
"sent_at": "2026-02-12T10:30:06.000Z",
"opened_at": null,
"revoked_at": "2026-02-12T10:40:00.000Z"
}
}
},
"meta": {
"type": "com.docue.sign.v1.invitation.revoked",
"id": "0196ae5b-2e9d-729f-aba5-fa4db6f8841b",
"timestamp": "2026-02-12T10:40:00.000Z"
}
}
}When a secret is configured, X-Webhook-Signature is the lowercase hexadecimal HMAC-SHA256 digest of the X-Webhook-Sent-At value, a dot, and the exact raw request body:
X-Webhook-Signature = hex(HMAC-SHA256(secret, X-Webhook-Sent-At + "." + raw_request_body))Validate the signature before parsing JSON. Do not compute it from a parsed and re-serialized body, because changes to whitespace, property order, or escaping change the digest. Compare signatures in constant time.
Because the send time is covered by the signature, you can use it to reject replayed requests. After the signature is valid, reject the request if X-Webhook-Sent-At is more than five minutes away from your current time. Each retry is sent with a fresh X-Webhook-Sent-At and signature, so legitimate retries pass this check. It does not replace idempotency: still ignore events whose event.meta.id you have already processed.
This Express example receives the raw body, validates it, and only then processes the event:
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.DOCUE_WEBHOOK_SECRET;
if (!secret) throw new Error("DOCUE_WEBHOOK_SECRET is required");
app.post(
"/webhooks/signing",
express.raw({ type: "application/json" }),
(request, response) => {
const signature = request.get("X-Webhook-Signature") ?? "";
const sentAt = request.get("X-Webhook-Sent-At") ?? "";
const expected = createHmac("sha256", secret)
.update(`${sentAt}.`)
.update(request.body)
.digest("hex");
const valid = /^[a-f0-9]{64}$/.test(signature)
&& timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
if (!valid) return response.sendStatus(401);
// Reject replays: the send time is signed, so it can be trusted here.
const ageMs = Math.abs(Date.now() - Date.parse(sentAt));
if (!(ageMs <= 5 * 60 * 1000)) return response.sendStatus(401);
const { event } = JSON.parse(request.body.toString("utf8"));
// Ignore the event if event.meta.id has already been processed.
console.log(event.meta.type, event.payload);
return response.sendStatus(204);
},
);Mount this route before any JSON body parser so that request.body contains the original bytes.