All articles
developersapiwebhooksintegration

A complete guide to setting up Signvoy webhooks — subscribe to document lifecycle events, verify HMAC signatures, handle retries, and build reliable integrations.

Signvoy TeamJuly 1, 2026

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:

EventWhen it fires
document.sentA document has been sent to all recipients
document.completedAll recipients have signed — the final PDF is ready
document.declinedA recipient declined to sign, cancelling the document
document.voidedThe document owner voided the document
document.expiredThe signing deadline passed without all recipients signing
recipient.signedAn individual recipient completed their signing step
recipient.declinedAn 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:

bash
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:

json
{
  "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:

HeaderDescription
Signvoy-EventThe event type (e.g. document.completed)
Signvoy-Delivery-IdUnique ID for this delivery attempt
Signvoy-Event-IdStable ID for this logical event — same across retries
Signvoy-TimestampUnix timestamp (seconds) when the delivery was attempted
Signvoy-Signaturet=<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>").

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):

typescript
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)

typescript
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:

typescript
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

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:

AttemptDelay after failure
1st retry30 seconds
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th attemptFinal attempt

After 5 failed attempts a delivery is marked exhausted. You can manually resend individual deliveries from the Webhooks dashboard or via the API:

bash
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:

typescript
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

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