Webhooks guide
Webhooks
Signvoy delivers real-time event notifications to your server over HTTPS. Register an endpoint, verify the HMAC-SHA256 signature on every request, and react to signing events without polling.
Event types
Subscribe to one or more events when registering your endpoint. Pass an empty enabledEvents array to receive all current and future events.
| Event | Description |
|---|---|
document.sent | Document transitioned to sent state — signing links emailed to recipients |
document.completed | All recipients have signed; signed PDF + certificate are ready for download |
document.declined | A recipient declined to sign |
document.voided | The document owner voided the document |
document.expired | The document expired before all recipients signed |
webhook.test | Fired by POST /v1/webhooks/:id/ping to verify the endpoint is reachable |
Register an endpoint
Pass an HTTPS URL and the events you want to subscribe to.
curl -X POST https://api.signvoy.com/v1/webhooks \
-H "Authorization: Bearer $SIGNVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/signvoy",
"description": "Production webhook",
"enabledEvents": [
"document.completed",
"document.declined"
]
}'secret field — a whsec_… prefixed HMAC-SHA256 signing secret. Store it securely — it is shown only once and is required to verify incoming deliveries.Understand the payload
Every delivery is an HTTP POST with a JSON body:
{
"id": "evt_01j9kfqn4xpw8m5tzx2vdk3b7a",
"type": "document.completed",
"createdAt": "2026-07-03T10:45:22Z",
"workspaceId": "ws_uuid...",
"data": {
"documentId": "doc_uuid...",
"status": "completed",
"completedAt": "2026-07-03T10:45:20Z"
}
}The id field is stable across retries — use it to deduplicate events on your end.
Verify the signature
Signvoy signs every delivery with HMAC-SHA256 and sends the signature in the Signvoy-Signature header. Always verify this before processing — never trust event payloads without verification.
import { createHmac, timingSafeEqual } from "node:crypto";
/** Verify a Signvoy webhook delivery.
* @param rawBody The raw request body bytes (do NOT parse first).
* @param header The "Signvoy-Signature" header value.
* @param secret The whsec_… secret from webhook creation.
*/
export function verifySignvoyWebhook(
rawBody: Buffer | string,
header: string,
secret: string,
): boolean {
// Header format: "t=<timestamp>,v1=<sig>"
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const timestamp = parts["t"];
const signature = parts["v1"];
if (!timestamp || !signature) return false;
// Re-derive the expected signature
const payload = `${timestamp}.${rawBody.toString()}`;
const expected = createHmac("sha256", secret)
.update(payload)
.digest("hex");
// Timing-safe comparison prevents timing-oracle attacks
return timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex"),
);
}
// ── Express usage ────────────────────────────────────────────────
app.post(
"/webhooks/signvoy",
express.raw({ type: "application/json" }),
(req, res) => {
const valid = verifySignvoyWebhook(
req.body,
req.headers["signvoy-signature"] as string,
process.env.SIGNVOY_WEBHOOK_SECRET!,
);
if (!valid) return res.status(401).send("Invalid signature");
const event = JSON.parse(req.body.toString());
if (event.type === "document.completed") {
// queue async processing here
}
res.status(200).send("OK");
},
);Retries and reliability
Stop chasing signatures
Join thousands of teams using Signvoy to close deals, onboard clients, and sign contracts — faster.
No credit card · Free forever plan · 14-day Pro trial
