Webhooks
Every event in the trail, delivered to an HTTPS endpoint you control, signed with a secret you were handed once.
Events
| Event | When |
|---|---|
| signing.created | A document was prepared. |
| signing.sent | It went out, with one link per signer. |
| signing.delivered | The message reached the provider. |
| signing.viewed | A signer opened it. The first view only — a trail that grows on every refresh buries the events that matter. |
| signing.consented | A signer agreed to do business electronically. Always before the signature. |
| signing.signed | A signer signed. |
| signing.completed | Everyone who had to sign has. This is the one most integrations want. |
| signing.declined | A signer said no, with a reason. |
| signing.voided | Withdrawn, with a reason. |
| signing.expired | The link ran out. |
| signing.reminded | A reminder went out, with a fresh link. |
| packet.ready | Every required document about one subject is complete. |
| packet.blocked | Something in a packet was declined. |
| signing.ping | A test delivery you asked for. |
Subscribe to "*" for all of them, or name the ones you want. An
event that is not on this list is refused when you add the endpoint, rather
than silently never arriving.
The payload
Enough to act on without a second call — and never the token. A webhook body lands in somebody's logs.
{
"event": "signing.completed",
"occurred_at": "2026-10-03T14:22:09Z",
"document": {
"id": "…", "title": "Rental agreement — Dana Reyes", "status": "completed",
"body_sha256": "…", "completed_at": "2026-10-03T14:22:09Z",
"metadata": { "external_ref": "RES-88213" }
},
"signer": { "email": "dana@example.com", "full_name": "Dana Reyes", "status": "signed" }
}
Retries
A failed delivery is retried on a widening ladder — 1 minute, 5, 30, 2 hours,
12, then daily — six times by default. After that the delivery is
dead and stays readable, so you can see what was missed rather
than guessing.
Twenty consecutive failures across deliveries pause the endpoint. An endpoint that has been dead for a week should not keep a queue growing; turning it back on forgives the count.
Deliveries are at-least-once. A network can drop our side of a successful call, so make your handler idempotent — the event carries enough to recognise a repeat.
Verifying a delivery
Three headers come with every delivery.
| Header | What |
|---|---|
| SignSealer-Event-Id | The delivery's id. The same on every retry, so it is what you deduplicate on. |
| SignSealer-Timestamp | Unix seconds when it was signed. Refuse anything more than five minutes from your clock, either way. |
| SignSealer-Signature | v1=<hex>: HMAC-SHA256 of timestamp.body with your secret. During a rotation it carries two, comma-separated; either verifies. |
The bytes that are signed are the exact bytes that are sent, so verify the raw body before parsing it, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, body, headers) {
const ts = Number(headers["signsealer-timestamp"]);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = createHmac("sha256", secret).update(ts + "." + body).digest("hex");
return headers["signsealer-signature"].split(",").some((part) => {
const given = /^v1=([0-9a-f]{64})$/.exec(part.trim())?.[1];
return given !== undefined && timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex"));
});
}
Rotating the secret
POST /v1/webhooks/{id}/rotate, or the button on the endpoint's page,
returns a new secret once. For the next 24 hours every delivery is signed with
both secrets — the header carries two v1= values, the new one first —
so you can switch your receiver at your own pace without dropping a delivery.
Then the old secret is forgotten. The check above already handles the pair.
Sending a delivery again
GET /v1/webhooks/{id}/deliveries lists the last fifty. A delivery
that was delivered, failed or died can be queued again with
POST /v1/webhooks/deliveries/{id}/replay, or "Send again" beside it in
the portal: a new delivery with the same event and the same body, with
replay_of naming the original. It gets a new event id, on purpose —
a receiver that already acted on the original can tell this repeat was asked for
by a person. One replay of a delivery may be in the queue at a time, and a replay
waits like any other if the endpoint is off or paused.
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 25 agreements a month.
Get an API key