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
Hören Sie auf, Unterschriften hinterherzurennen
Schließen Sie sich Tausenden von Teams an, die Signvoy nutzen, um Deals abzuschließen, Kunden zu gewinnen und Verträge zu unterzeichnen — schneller.
Keine Kreditkarte · Dauerhaft kostenloser Plan · 14 Tage Pro-Test
