May 21, 2026 · Developer Guide
E-Signature Webhook Events: Know the Second a Document is Signed
Polling an API every 30 seconds to check if someone signed your contract is embarrassing. Here's how to set up webhooks that tell you the instant a document is viewed, signed, or completed — with HMAC verification to prevent spoofing.
Founder, Signbee
TL;DR
Signbee sends 4 webhook events: document.sent, document.viewed, document.signed, document.completed. Each is signed with HMAC-SHA256 so you can verify it's genuinely from Signbee. Retries happen automatically with exponential backoff. Compare that to DocuSign Connect, which requires XML parsing, envelope-level subscriptions, and a separate configuration portal.
Why webhooks, not polling
I've seen production codebases that poll DocuSign's API every 30 seconds to check if a document was signed. It's wasteful, it's slow (up to 30 seconds of latency), and it eats into your rate limits. If you're sending 100 documents per day and polling each one every 30 seconds until it's signed, that's thousands of unnecessary API calls.
Webhooks invert the model. The API calls you when something happens. Zero latency. Zero wasted requests. Your server receives a POST request within seconds of the event occurring, processes it, and moves on.
Signbee's webhook event types
Every document in Signbee progresses through a lifecycle, and each stage fires a webhook event:
| Event | When it fires |
|---|---|
| document.sent | Signing email is delivered |
| document.viewed | Recipient opens signing link |
| document.signed | Recipient applies their signature |
| document.completed | Signed PDF + certificate ready |
The webhook payload
Every webhook event sends a JSON payload to your registered endpoint. Here's what a document.completed event looks like:
{
"event": "document.completed",
"document_id": "doc_a1b2c3d4e5f6",
"timestamp": "2026-05-21T14:32:18.000Z",
"data": {
"recipient_name": "Jane Smith",
"recipient_email": "jane@example.com",
"signer_ip": "203.0.113.42",
"signed_at": "2026-05-21T14:32:15.000Z",
"document_hash": "a3f2b8c1d4e5f678...",
"signed_hash": "9c8d7e6f5a4b3c21...",
"certificate_id": "cert_x9y8z7w6",
"download_url": "https://signb.ee/api/v1/documents/doc_a1b2c3d4e5f6/download"
}
}The payload includes the SHA-256 hashes for both the original and signed document, so you can store them in your database for independent verification. For more on how the hashing works, see our audit trail deep-dive.
Verifying webhook signatures with HMAC-SHA256
Anyone can send a POST request to your webhook URL. Without verification, an attacker could forge a "document.completed" event and trick your system into thinking a contract was signed when it wasn't. This is why every webhook must be verified.
Signbee signs each webhook payload with HMAC-SHA256 using your webhook secret. The signature is included in the X-Signbee-Signature header.
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhookSignature(
rawBody: string,
signature: string,
secret: string
): boolean {
const expected = createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Timing-safe comparison prevents timing attacks
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express.js example
app.post("/webhooks/signbee", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-signbee-signature"] as string;
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, process.env.SIGNBEE_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
switch (event.event) {
case "document.completed":
// Archive signed PDF, update database, notify team
handleDocumentCompleted(event.data);
break;
case "document.viewed":
// Track engagement
handleDocumentViewed(event.data);
break;
}
res.status(200).json({ received: true });
});Critical: Always use timingSafeEqual for signature comparison. Regular string comparison (===) leaks timing information that could allow an attacker to forge valid signatures character by character.
Sending a document and receiving the webhook
Here's the full flow — send a document via the API, then handle the webhook when it's signed:
// Step 1: Send the document
const response = await fetch("https://signb.ee/api/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer sb_live_your_api_key",
},
body: JSON.stringify({
markdown: "# Freelance Agreement\n\nThis agreement is between...",
recipient_name: "Alex Chen",
recipient_email: "alex@studio.co",
}),
});
const { document_id } = await response.json();
// Store document_id in your database
// Step 2: Your webhook handler receives events automatically
// No polling. No cron jobs. No setTimeout loops.Retry logic and failure handling
Your webhook endpoint might be temporarily down — deploying, restarting, or experiencing a network issue. Signbee handles this with automatic retries:
If all retries fail, the event appears in your Signbee dashboard as "failed" and can be manually re-triggered.
How this compares to DocuSign Connect
DocuSign's webhook equivalent is called DocuSign Connect. It works, but the developer experience is significantly more complex:
| Aspect | DocuSign Connect | Signbee Webhooks |
|---|---|---|
| Payload format | XML | JSON |
| Configuration | Admin console + API + per-envelope | Dashboard URL + secret |
| Event granularity | Envelope-level (100+ event types) | 4 document-level events |
| Signature verification | HMAC (added 2023) | HMAC-SHA256 (from day one) |
| Retry strategy | Configurable (complex) | Automatic exponential backoff |
| Setup time | Hours | Minutes |
The biggest friction with DocuSign Connect is the XML payload format. In 2026, parsing XML in a Node.js application requires a third-party library (xml2js, fast-xml-parser) and adds complexity that JSON webhooks simply don't have.
Common webhook mistakes to avoid
Not responding with 200 quickly. Your webhook handler should return 200 immediately, then process the event asynchronously. If your handler takes longer than 10 seconds (e.g., downloading a PDF, sending emails), the delivery will be marked as failed and retried.
Not handling duplicates. Due to retries and network conditions, you may receive the same event more than once. Use the document_id + event combination as an idempotency key. If you've already processed it, return 200 and skip processing.
Not verifying signatures. Every webhook must be verified with HMAC-SHA256. Without verification, anyone who discovers your webhook URL can forge events. This is especially dangerous for document.completed events — a forged event could trick your system into provisioning access or closing a deal based on an unsigned document.
For the broader API architecture including webhooks, see our comprehensive webhooks setup guide and the API documentation.
Frequently Asked Questions
What webhook events does Signbee send?
Four events: document.sent, document.viewed, document.signed, and document.completed. Each includes the document ID, timestamp, and relevant metadata.
How do I verify webhook signatures?
Compute HMAC-SHA256 of the raw request body using your webhook secret, then compare to the X-Signbee-Signature header using a timing-safe comparison function. Never use === for signature comparison.
What if my webhook endpoint is down?
Signbee retries with exponential backoff: 1min, 5min, 30min, 2h, 24h. After 6 failed attempts, the event is marked as failed in your dashboard and can be manually re-triggered.
Real-time signing notifications — 5 free docs/month.
Last updated: May 21, 2026 · Michael Beckett is the founder of Signbee and B2bee Ltd.