A complete guide to setting up Signvoy webhooks — subscribe to document lifecycle events, verify HMAC signatures, handle retries, and build reliable integrations.
Webhooks let your application react to Signvoy events in real time — no polling required. When a document is signed, completed, or declined, Signvoy POSTs a signed JSON payload to any HTTPS URL you register. This guide covers everything you need to go from zero to a production-grade integration.
What are webhooks?
Instead of repeatedly asking "has anything changed?" (polling), webhooks let Signvoy tell you exactly when something happens. Your server receives an HTTP POST request with a JSON body describing the event, and you can act on it immediately — trigger a workflow, update a database, send a Slack notification, or unlock a next step in your product.
Available events
Signvoy emits the following events to your webhook endpoints:
| Event | When it fires |
|---|---|
document.sent | A document has been sent to all recipients |
document.completed | All recipients have signed — the final PDF is ready |
document.declined | A recipient declined to sign, cancelling the document |
document.voided | The document owner voided the document |
document.expired | The signing deadline passed without all recipients signing |
recipient.signed | An individual recipient completed their signing step |
recipient.declined | An individual recipient declined (before the doc is voided) |
You can subscribe to any combination of events — or leave the list empty to receive all events.
Setting up an endpoint
1. Register your endpoint
In your Signvoy workspace, go to Settings → Webhooks and click Add endpoint. Enter your HTTPS URL and select the events you want to receive.
You can also register endpoints via the API:
curl -X POST https://api.signvoy.com/v1/webhooks \
-H "Authorization: Bearer $SIGNVOY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/signvoy",
"description": "Production webhook",
"enabledEvents": ["document.completed", "document.declined"]
}'
2. Save the signing secret
When you create an endpoint, Signvoy returns a signing secret starting with whsec_. This secret is shown only once — copy it immediately and store it securely (e.g. as an environment variable SIGNVOY_WEBHOOK_SECRET).
You'll use this secret to verify that incoming requests actually came from Signvoy.
Webhook payload structure
Every event has the same envelope:
{
"event": "document.completed",
"createdAt": "2026-07-01T12:34:56.789Z",
"data": {
"document": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"workspaceId": "...",
"name": "NDA — Acme Corp",
"status": "completed",
"completedAt": 1751370896789,
"recipients": [
{
"id": "...",
"name": "Jane Smith",
"email": "jane@example.com",
"status": "signed",
"signedAt": 1751370896000
}
]
}
}
}
For recipient-level events (recipient.signed, recipient.declined), the data object also includes a recipient field for the specific signer that triggered the event.
Request headers
Every Signvoy webhook request includes these headers:
| Header | Description |
|---|---|
Signvoy-Event | The event type (e.g. document.completed) |
Signvoy-Delivery-Id | Unique ID for this delivery attempt |
Signvoy-Event-Id | Stable ID for this logical event — same across retries |
Signvoy-Timestamp | Unix timestamp (seconds) when the delivery was attempted |
Signvoy-Signature | t=<timestamp>,v1=<hex-hmac> |
Verifying signatures
Always verify the Signvoy-Signature header before processing a webhook. This ensures the request came from Signvoy and the payload hasn't been tampered with.
The signature is HMAC-SHA256(secret, "<timestamp>.<raw-body>").
With the @signvoy/node SDK (recommended)
The easiest approach — the SDK ships a verifyWebhookSignature helper that handles parsing, timing-safe comparison, and replay-attack prevention (rejects requests older than 5 minutes):
import { verifyWebhookSignature } from "@signvoy/node/webhooks";
const valid = verifyWebhookSignature(
rawBody, // Buffer | string
req.headers["signvoy-signature"] as string,
process.env.SIGNVOY_WEBHOOK_SECRET!,
);
if (!valid) return res.status(401).json({ error: "Invalid signature" });
Express.js example (with SDK)
import express from "express";
import { verifyWebhookSignature } from "@signvoy/node/webhooks";
const app = express();
// Use raw body parser — signature verification requires the unparsed bytes.
app.post(
"/webhooks/signvoy",
express.raw({ type: "application/json" }),
(req, res) => {
const valid = verifyWebhookSignature(
req.body,
req.headers["signvoy-signature"] as string,
process.env.SIGNVOY_WEBHOOK_SECRET!,
);
if (!valid) return res.status(401).json({ error: "Invalid signature" });
const event = JSON.parse(req.body.toString());
switch (event.type) {
case "document.completed":
console.log("Document completed:", event.data.documentId);
// Trigger your post-signing workflow here
break;
case "document.declined":
console.log("Document declined:", event.data.documentId);
break;
}
// Always respond quickly — Signvoy retries on non-2xx or timeouts.
res.status(200).json({ received: true });
},
);
Without the SDK — Node.js / TypeScript
If you prefer not to add the dependency, you can verify the signature manually:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignvoyWebhook(
rawBody: Buffer | string,
signatureHeader: string,
secret: string,
): boolean {
// Parse "t=<ts>,v1=<hex>"
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=")),
);
const timestamp = parts["t"];
const receivedHex = parts["v1"];
if (!timestamp || !receivedHex) return false;
// Reject requests older than 5 minutes to prevent replay attacks.
const age = Math.floor(Date.now() / 1000) - Number.parseInt(timestamp, 10);
if (Math.abs(age) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody.toString()}`)
.digest("hex");
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(receivedHex, "hex"),
);
}
Python
import hashlib
import hmac
import time
def verify_signvoy_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts.get("t")
received_hex = parts.get("v1")
if not timestamp or not received_hex:
return False
# Reject requests older than 5 minutes.
age = abs(int(time.time()) - int(timestamp))
if age > 300:
return False
signed_payload = f"{timestamp}.{raw_body.decode()}"
expected = hmac.new(secret.encode(), signed_payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_hex)
Retry behaviour
If your endpoint returns a non-2xx status code or times out (15-second limit), Signvoy will retry automatically with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th attempt | Final attempt |
After 5 failed attempts a delivery is marked exhausted. You can manually resend individual deliveries from the Webhooks dashboard or via the API:
curl -X POST https://api.signvoy.com/v1/webhooks/<endpoint-id>/deliveries/<delivery-id>/retry \
-H "Authorization: Bearer $SIGNVOY_API_KEY"
If an endpoint accumulates too many consecutive failures, Signvoy will auto-disable it and notify you. Re-enable it from the dashboard once you've fixed the underlying issue.
Idempotency
Use the Signvoy-Event-Id header to deduplicate events. The same logical event always carries the same Signvoy-Event-Id, even across retries and manual resends. Store processed event IDs and skip duplicates:
const eventId = req.headers["signvoy-event-id"];
if (await db.eventAlreadyProcessed(eventId)) {
return res.status(200).json({ received: true }); // skip duplicate
}
await db.markEventProcessed(eventId);
// ... handle event
Testing
Use the Send test event button in the Webhooks dashboard (or POST /v1/webhooks/:id/ping) to fire a webhook.test event to your endpoint and see the delivery result immediately — without needing a real document signing flow.
Next steps
- Explore the full Webhooks API reference to manage endpoints programmatically
- Read the API quickstart to create templates and send documents via the REST API
- Questions? Reach out via the contact page
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
