A signed document sending a gold signal into a black cube
Founder's Engineering LogUpdated September 2026 · 10 min read

We Shipped Webhooks: Why We Built a One-Event Architecture

For months, the honest positioning was that Signbee handles end-to-end email delivery, so developers didn't need webhooks. That was true for simple two-party agreements. But as autonomous AI agents, automated CRM pipelines, and multi-tenant SaaS platforms began processing thousands of contracts through Signbee, active polling became a bottleneck. Here is why we shipped webhooks—and why we restricted our entire delivery catalog to exactly one event.

Michael Beckett
Michael Beckett

Founder, Signbee

1

Event Type

0 Retries

Storm Defense

GFM

Markdown Tables

HMAC-256

Per-User Secret

Release Overview (TL;DR)

Pro and Business tiers can now pass webhook_url on POST /api/v1/send. The response yields an immutable webhook_secret. When counterparties sign, Signbee POSTs document.signed with an HMAC-SHA256 signature in X-Signbee-Signature. Free accounts receive an explicit 403 error. No retry storms—if you miss the POST, poll GET /api/v1/documents/{id}. Additionally, GitHub Flavored Markdown (GFM) pipe tables now render across all plans.

The Philosophy of Event Minimalism: One Event vs The 24-Event Catalog

When looking at legacy document APIs, you will notice sprawling webhook catalogs with dozens of event triggers:

PlatformCatalog BreadthConsuming ComplexityArchitectural Philosophy
Signbee1 Event (document.signed)Low (Zero state drift)Event minimalism: notify only when legal contracts transition to executed.
DocuSign Connect24+ Events (sent, delivered, viewed...)High (Out-of-order race conditions)Enterprise telemetry firehose requiring complex queue deduplication.
PandaDoc12 Events (draft, sent, viewed...)Medium (Requires state machines)CRM status sync for pipeline stages.

In practice, having 20+ events forces developers to write complex state machines defending against out-of-order execution (for example, receiving recipient.viewed after envelope.completed due to network routing variations). By emitting only document.signed, we ensure that every incoming webhook is actionable, definitive, and final.

The Security Model: Timing-Safe HMAC Verification

Never trust an incoming HTTP POST without cryptographic proof of origin. When Signbee sends a webhook, we compute the HMAC-SHA256 signature using your per-user secret:

Node.js / Express — Timing-Safe Webhook Receiver
import express from "express";
import crypto from "crypto";

const app = express();

// Capture raw body buffer for accurate HMAC hashing
app.use(express.raw({ type: "application/json" }));

app.post("/webhooks/signbee", (req, res) => {
  const signature = req.headers["x-signbee-signature"] as string;
  const webhookSecret = process.env.SIGNBEE_WEBHOOK_SECRET!;

  if (!signature) {
    return res.status(401).send("Missing signature header");
  }

  // Compute expected HMAC digest from raw buffer
  const expectedHash = crypto
    .createHmac("sha256", webhookSecret)
    .update(req.body)
    .digest("hex");

  const expectedBuffer = Buffer.from(expectedHash, "utf8");
  const signatureBuffer = Buffer.from(signature, "utf8");

  // Constant-time comparison prevents side-channel timing attacks
  if (
    expectedBuffer.length !== signatureBuffer.length ||
    !crypto.timingSafeEqual(expectedBuffer, signatureBuffer)
  ) {
    return res.status(401).send("Invalid signature");
  }

  const payload = JSON.parse(req.body.toString("utf8"));
  console.log(`✅ Verified completion for document ${payload.document_id}`);
  console.log(`PDF Download URL: ${payload.signed_pdf_url}`);
  console.log(`SHA-256 Digest: ${payload.signature_hash}`);

  // Respond immediately with 200 OK
  res.status(200).json({ received: true });
});

For teams running Python or FastAPI services, the same timing-safe verification pattern applies using standard library hmac and hashlib primitives:

Python / FastAPI — Cryptographic Webhook Handler
from fastapi import FastAPI, Request, HTTPException, status
import hmac
import hashlib
import os

app = FastAPI()
WEBHOOK_SECRET = os.environ["SIGNBEE_WEBHOOK_SECRET"].encode("utf-8")

@app.post("/webhooks/signbee", status_code=status.HTTP_200_OK)
async def handle_signbee_webhook(request: Request):
    signature = request.headers.get("x-signbee-signature")
    if not signature:
        raise HTTPException(status_code=401, detail="Missing signature header")

    # Read raw body bytes to compute HMAC-SHA256
    raw_body = await request.body()
    computed_digest = hmac.new(
        WEBHOOK_SECRET,
        raw_body,
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison prevents side-channel timing leaks
    if not hmac.compare_digest(computed_digest, signature):
        raise HTTPException(status_code=401, detail="Invalid cryptographic signature")

    payload = await request.json()
    doc_id = payload.get("document_id")
    pdf_url = payload.get("signed_pdf_url")
    cert_hash = payload.get("signature_hash")

    # Dispatch to background task or message broker
    print(f"Verified contract completion: {doc_id} with SHA-256 seal {cert_hash}")
    return {"received": True, "document_id": doc_id}

The Anatomy of the document.signed Payload

When a counterparty submits their final signature, Signbee compiles the document into an immutable PDF, computes the SHA-256 cryptographic digest, and dispatches an HTTP POST request with the following JSON schema:

Webhook JSON Payload Schema
{
  "event": "document.signed",
  "document_id": "doc_8f492b7194ab",
  "status": "completed",
  "created_at": "2026-09-04T14:15:22.000Z",
  "completed_at": "2026-09-04T14:18:47.000Z",
  "title": "Master Services Agreement — Acme Corp",
  "signed_pdf_url": "https://api.signb.ee/v1/documents/doc_8f492b7194ab/download?token=ephemeral_38f29...",
  "certificate_url": "https://api.signb.ee/v1/documents/doc_8f492b7194ab/certificate?token=ephemeral_38f29...",
  "signature_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "counterparties": [
    {
      "email": "sarah@acmecorp.com",
      "name": "Sarah Connor",
      "role": "signer_1",
      "signed_at": "2026-09-04T14:16:10.000Z",
      "ip_address": "198.51.100.42",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..."
    },
    {
      "email": "finance@provider.com",
      "name": "Alex Miller",
      "role": "signer_2",
      "signed_at": "2026-09-04T14:18:47.000Z",
      "ip_address": "203.0.113.19",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)..."
    }
  ],
  "metadata": {
    "deal_id": "crm_98124",
    "account_tier": "enterprise"
  }
}
FieldTypeDescription
eventstringAlways document.signed. Indicates definitive contract finalization.
signed_pdf_urlstringSigned ephemeral URL to fetch the tamper-sealed vector PDF directly.
certificate_urlstringDirect link to download the standalone Certificate of Completion audit log.
signature_hashstring (hex)The SHA-256 cryptographic digest of the final PDF file bytes.
counterpartiesarray[object]Detailed audit log including timestamp, IP address, and browser agent per signer.
metadataobjectEchoes back custom key-value pairs passed during initial document dispatch.

Why Zero Retries Defeats the Distributed Thundering Herd

Most SaaS APIs proudly boast about complex exponential backoff retry engines that will re-attempt failed webhook deliveries for 72 hours. In high-throughput distributed systems, this is an antipattern that causes severe cascading failures.

Consider a common production scenario: your application server undergoes a database migration or traffic spike that causes inbound requests to return HTTP 503 for 12 minutes. Under traditional retry regimes, every contract signed during those 12 minutes is queued for multiple automated retries. When your application recovers, it is immediately crushed by a massive backlog of retries arriving simultaneously—a self-inflicted Distributed Denial of Service (DDoS) known as the thundering herd problem.

The Dual-Rail Architecture: Instant Event + Polling Fallback

Signbee implements a clean dual-rail architecture that delivers 100% reliability without retry storms:

Rail 1: Real-Time Webhook

Delivered within 500ms of document execution. Handles 99.8% of typical production workflows instantly.

Rail 2: Deterministic GET Polling

If your server was offline, a periodic background worker checks GET /api/v1/documents/{id} and recovers state idempotently.

Markdown GFM Tables: Native Vector Grid Rendering

Alongside webhooks, we rolled out full support for GitHub Flavored Markdown (GFM) pipe tables. Tables render automatically across every subscription tier (including Free):

Markdown Invoice Table Example
| Item Description                     | Units | Unit Price | Total Amount |
|--------------------------------------|-------|------------|--------------|
| Cloud Architecture Audit             | 1     | $3,500.00  | $3,500.00    |
| Multi-Agent MCP Server Integration   | 1     | $4,500.00  | $4,500.00    |
| Annual Infrastructure SLA Support   | 12    | $250.00    | $3,000.00    |

Tables automatically handle cell wrapping, column alignments, and page break calculations with zero custom CSS required.

Frequently Asked Questions

Why does Signbee support only a single webhook event (document.signed)?

In distributed systems, excessive fine-grained webhook events (such as document.created, envelope.sent, recipient.viewed, tab.focused) create significant architectural debt for consuming backends. Teams must build complex state reconciliation logic, manage out-of-order delivery across network edges, and defend against duplicate events. In 99% of business contract integrations, software workflows only care about a single state transition: has the contract been executed with a legal audit trail? By emitting exactly one event—document.signed—Signbee guarantees that your webhook listener remains deterministic, lightweight, and completely free of intermediate state corruption.

How do developers verify incoming webhook signatures?

Every webhook request includes an X-Signbee-Signature HTTP header containing the hexadecimal HMAC-SHA256 digest of the raw JSON request body. To verify authenticity, your backend must compute the HMAC-SHA256 hash across the unparsed request bytes using the per-user webhook_secret returned during the initial send call. Crucially, you must perform constant-time comparison using crypto.timingSafeEqual() in Node.js or hmac.compare_digest() in Python to eliminate vulnerability to side-channel timing attacks.

What happens if our receiving server is temporarily offline when a webhook fires?

Signbee deliberately enforces a fast, 10-second fire-and-forget delivery strategy without executing automated exponential retry storms that can trigger cascading denial-of-service failures during customer outages. If your server is offline, undergoing a redeployment, or unreachable, your backend should simply execute an active poll against GET /api/v1/documents/{id}. When the document is completed, the GET endpoint returns the exact same signature metadata, certified PDF download URL, and SHA-256 cryptographic seal.

Are GitHub Flavored Markdown (GFM) tables supported in rendered PDFs?

Yes. Along with the webhook engine, full support for GitHub Flavored Markdown (GFM) pipe tables is enabled natively across all subscription tiers, including the Free developer plan. Tables are compiled directly into vector-formatted PDF grids with automated page breaks, proportional cell widths, and clean border styling, making it effortless to generate dynamic invoices, fee schedules, and equipment manifests without requiring raw HTML or CSS templates.

Upgrade to Pro to Enable Webhooks

Receive instant document.signed notifications directly into your serverless functions and agent workflows.