Agent Document Signing Webhooks: Get document.signed Without Polling
Autonomous agents and automated API pipelines require event-driven callbacks when human signers finish agreements. Pass webhook_url on POST /api/v1/send. Available on Pro and Business plans. Free returns 403 — we never silently drop webhook configurations.
Founder, Signbee
Your autonomous AI agent generates and dispatches an agreement, then halts its execution loop to avoid wasting serverless compute and LLM tokens. With Signbee webhooks, your agent backend is reactively re-awakened the millisecond the recipient executes the signature. On Pro or Business plans, append webhook_url to your initial payload. Secure your receiver using the returned webhook_secret (whsec_…). When the ceremony concludes, Signbee POSTs exactly once with the document.signed event, signed cryptographically via X-Signbee-Signature.
The Polling Tax vs Reactive Event Delivery
Autonomous agents operating inside long-running loops or serverless workers (like AWS Lambda, Cloudflare Workers, or Google Cloud Run) face a fundamental architectural penalty when polling for human actions. Humans sign contracts on human time: sometimes within three minutes, often after four hours, and occasionally across three business days.
If your agent executes an active HTTP polling loop against GET /api/v1/documents/{id} every 30 seconds, it incurs significant architectural overhead:
| Dimension | HTTP Polling Loop (30s) | Signbee Webhook Push |
|---|---|---|
| Notification Latency | Average 15,000 ms (0–30s window) | < 250 ms edge delivery |
| Compute Consumption | Continuous worker sleep/wake cycles | Zero compute until triggered |
| Network Egress / Ingress | 2,880 HTTP round-trips / 24h per doc | Exactly 1 outbound POST request |
| LLM Context Retention | Requires persistent state thread or DB loop | Stateless reactivation via event handler |
| API Rate Limit Impact | Consumes rate quotas rapidly at scale | Zero rate limit consumption |
While polling remains the recommended choice for local scripts or CLI tools behind restrictive NAT gateways with no public IP, enterprise workflows and production AI agents require reactive webhook callbacks.
Event Minimalism: Signbee vs Enterprise Monoliths
Legacy e-signature platforms flood webhook receivers with granular, redundant lifecycle events. An application integrating DocuSign Connect or Adobe Sign typically receives dozens of noisy webhooks for every envelope, requiring complex state reducers to filter out noise.
| Platform | Lifecycle Events Catalog | State Complexity | Configuration Overhead |
|---|---|---|---|
| Signbee | 1 deterministic event: document.signed | Zero state tracking needed | Pass webhook_url in POST body |
| DocuSign Connect | 24+ events (sent, delivered, viewed, declined) | High: requires multi-stage state machine | Connect portal + HMAC keys + XML/JSON toggles |
| PandaDoc API | 12 events (document_state_changed, recipient_completed) | Moderate: parsing recipient arrays | Dashboard webhook subscription setup |
| Dropbox Sign | 16 events (signature_request_sent, signed, downloaded) | Moderate: custom event wrappers | App dashboard callback registration |
Signbee follows Unix philosophy: do one thing and do it flawlessly. When the document is fully executed, Signbee POSTs once with everything you need: the document ID, the cryptographic signature hash, and the direct download URL for the certified PDF.
Dispatching a Document with webhook_url
Adding a webhook callback requires no upfront dashboard configuration or developer portal registrations. You simply pass webhook_url directly in your existing POST /api/v1/send request payload.
curl -X POST https://signb.ee/api/v1/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"title": "Master Services Agreement",
"markdown": "# Master Services Agreement\n\nThis agreement governs services rendered...",
"recipient_name": "Bob Smith",
"recipient_email": "bob@enterprise.com",
"webhook_url": "https://api.yourdomain.com/webhooks/signbee"
}'{
"document_id": "cmm8902abc12345678",
"status": "pending_recipient",
"sender": "Alice Chen",
"recipient": "Bob Smith",
"expires_at": "2026-09-11T12:00:00.000Z",
"webhook_secret": "whsec_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d"
}Payload & Verification Checklist
- webhook_url: Fully-qualified HTTPS destination endpoint reachable over the public internet.
- webhook_secret: Unique per-user secret key prefixed with
whsec_, returned in the initial 200 response. Store this in your secure secrets store. - X-Signbee-Signature Header: The HMAC-SHA256 hex digest computed over the raw HTTP request body.
- Event Trigger: Dispatched immediately upon human signature completion and PDF seal stamping.
- GFM Tables: Fully supported inside the markdown field across all service tiers.
Free Plan Guardrails: HTTP 403 (No Swallowing)
Many SaaS APIs feature silent degradation bugs: if a user on an unsupported pricing tier passes an advanced parameter, the API silently strips the parameter, accepts the request, and proceeds. This creates catastrophic failures for autonomous AI agents, which assume the webhook was registered and sit in an infinite waiting state.
Signbee eliminates this failure mode entirely. If an account on the Free tier includes a webhook_url parameter, the API immediately halts with an explicit HTTP 403 Forbidden status code:
{
"error": "Webhooks require Pro or Business plan. Upgrade at https://signb.ee/dashboard",
"plan": "FREE"
}webhook_url. If your application relies on event callbacks, upgrade the account to Pro or Business. If you intend to operate on the Free tier, route your workflow through the polling pattern.The Inbound Webhook Payload Specification
When the recipient signs the document, Signbee initiates an outbound HTTP POST request to your designated webhook_url. The request includes standard HTTP headers and a self-contained JSON body:
Inbound HTTP Request Headers
Content-Type: application/jsonX-Signbee-Signature: [64-character hex HMAC-SHA256 digest]X-Signbee-Event: document.signedUser-Agent: Signbee-Webhook/1.0
{
"event": "document.signed",
"document_id": "cmm8902abc12345678",
"title": "Master Services Agreement",
"status": "signed",
"sender_email": "alice@startup.com",
"recipient_name": "Bob Smith",
"recipient_email": "bob@enterprise.com",
"recipient_signed_at": "2026-09-04T14:32:18.102Z",
"signed_pdf_url": "https://signb.ee/uploads/signed_cmm8902abc12345678.pdf",
"signature_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"verify_url": "https://signb.ee/verify/cmm8902abc12345678",
"timestamp": "2026-09-04T14:32:18.520Z"
}Note: status is strictly signed (not "completed" or "finished"). signed_pdf_url provides direct access to the certified document embedded with the SHA-256 cryptographic audit certificate.
Production Cryptographic Verification Implementations
To guard your application against spoofing attacks, replay injections, and unauthorized payload forgery, you must verify the X-Signbee-Signature header.
Crucially, you must compute the HMAC-SHA256 signature using the raw HTTP request buffer. If your web framework parses the body into an object and re-stringifies it, JSON key serialization differences will produce a hash mismatch.
1. Node.js & Express (TypeScript)
import express, { Request, Response } from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = process.env.SIGNBEE_WEBHOOK_SECRET || "whsec_...";
function verifySignbeeSignature(
rawBody: Buffer,
signatureHeader: string | undefined,
secret: string
): boolean {
if (!signatureHeader) return false;
const hmac = crypto.createHmac("sha256", secret);
hmac.update(rawBody);
const calculatedHex = hmac.digest("hex");
const calculatedBuf = Buffer.from(calculatedHex, "utf8");
const headerBuf = Buffer.from(signatureHeader, "utf8");
if (calculatedBuf.length !== headerBuf.length) {
return false;
}
return crypto.timingSafeEqual(calculatedBuf, headerBuf);
}
// Preserve raw body buffer for verification
app.post(
"/webhooks/signbee",
express.raw({ type: "application/json" }),
(req: Request, res: Response) => {
const signature = req.headers["x-signbee-signature"] as string | undefined;
if (!verifySignbeeSignature(req.body, signature, WEBHOOK_SECRET)) {
console.error("Signature verification failed.");
return res.status(401).json({ error: "Invalid signature" });
}
const payload = JSON.parse(req.body.toString("utf8"));
if (payload.event === "document.signed") {
console.log(`Document ${payload.document_id} signed by ${payload.recipient_name}`);
console.log(`Verified PDF URL: ${payload.signed_pdf_url}`);
console.log(`Tamper Hash: ${payload.signature_hash}`);
// Trigger downstream agent actions or notify database
}
return res.status(200).json({ received: true });
}
);
app.listen(3000, () => console.log("Webhook listener active on :3000"));2. Python (FastAPI / Starlette)
import hmac
import hashlib
import json
import os
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.responses import JSONResponse
app = FastAPI()
WEBHOOK_SECRET = os.environ.get("SIGNBEE_WEBHOOK_SECRET", "whsec_...").encode("utf-8")
@app.post("/webhooks/signbee")
async def handle_signbee_webhook(request: Request):
signature_header = request.headers.get("x-signbee-signature")
if not signature_header:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing signature header")
# Read the raw unparsed request body buffer
raw_body = await request.body()
# Compute expected HMAC-SHA256 hex digest
computed_hmac = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
# Perform constant-time string comparison to prevent timing attacks
if not hmac.compare_digest(computed_hmac, signature_header):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid HMAC signature")
payload = json.loads(raw_body.decode("utf-8"))
if payload.get("event") == "document.signed":
doc_id = payload.get("document_id")
pdf_url = payload.get("signed_pdf_url")
print(f"Contract executed: {doc_id} -> {pdf_url}")
# Dispatch event to Celery worker or agent memory pipeline
return JSONResponse(status_code=200, content={"received": True})3. Go (net/http)
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
)
var webhookSecret = []byte(os.Getenv("SIGNBEE_WEBHOOK_SECRET"))
type SignbeePayload struct {
Event string `json:"event"`
DocumentID string `json:"document_id"`
Title string `json:"title"`
Status string `json:"status"`
SignedPdfUrl string `json:"signed_pdf_url"`
SignatureHash string `json:"signature_hash"`
}
func handleSignbeeWebhook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
sigHeader := r.Header.Get("X-Signbee-Signature")
if sigHeader == "" {
http.Error(w, "Missing signature header", http.StatusUnauthorized)
return
}
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, webhookSecret)
mac.Write(rawBody)
expectedSig := hex.EncodeToString(mac.Sum(nil))
// Constant-time byte comparison prevents side-channel timing leaks
if subtle.ConstantTimeCompare([]byte(expectedSig), []byte(sigHeader)) != 1 {
http.Error(w, "Signature verification failed", http.StatusUnauthorized)
return
}
var payload SignbeePayload
if err := json.Unmarshal(rawBody, &payload); err != nil {
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
return
}
if payload.Event == "document.signed" {
log.Printf("Document completed: %s, PDF: %s", payload.DocumentID, payload.SignedPdfUrl)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received":true}`))
}
func main() {
http.HandleFunc("/webhooks/signbee", handleSignbeeWebhook)
log.Println("Go webhook receiver listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Architectural Best Practices for AI Agents
Acknowledge in Sub-500ms
Always return an HTTP 200 response immediately after signature verification. Offload heavy PDF archiving, vector embedding updates, or downstream model prompts to background worker queues (like BullMQ, SQS, or Celery) to prevent connection timeouts.
Idempotency via document_id
Design your webhook handler to be strictly idempotent. While Signbee fires once, network proxies or upstream retries could theoretically duplicate packets. Key your database mutation on payload.document_id to ensure downstream workflows execute exactly once.
Store the Cryptographic Hash
The signature_hash string delivered in the payload is the immutable SHA-256 digest of the finalized document. Store this value alongside your internal contract record to provide instant, court-admissible audit proof without re-hashing large PDF files.
Frequently Asked Questions
What webhook events does Signbee dispatch to client endpoints?
Signbee dispatches exactly one event: document.signed. Unlike legacy e-signature suites that emit dozens of noisy intermediate lifecycle signals (such as recipient_opened, email_delivered, document_viewed, or page_scrolled), Signbee adheres strictly to event minimalism. An AI agent or backend workflow only requires one deterministic notification: the exact instant when all required signers have legally executed the document and the cryptographic Certificate of Completion has been compiled. Signbee POSTs a single JSON payload directly to the webhook_url specified during initial document dispatch.
How do you securely verify the X-Signbee-Signature header in production?
To verify X-Signbee-Signature securely, you must compute an HMAC-SHA256 digest of the raw, unparsed HTTP request body using your secret webhook key (prefixed with whsec_) returned upon document creation. The computed digest must then be compared to the hex string delivered in the X-Signbee-Signature header using a timing-safe byte comparison function (such as crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python, or subtle.ConstantTimeCompare in Go). You must never parse the incoming payload into JSON and re-serialize it prior to hashing, because serialized key ordering or whitespace normalization differences will alter the binary hash and produce authentication false negatives.
Does the Signbee Free tier support webhook callbacks?
No. Webhook event delivery is exclusively supported on Signbee Pro and Business plans. If an API client on the Free tier passes a webhook_url parameter to POST /api/v1/send, the API immediately halts with an HTTP 403 Forbidden response returning error: Webhooks require Pro or Business plan. Signbee intentionally refuses to swallow or silently drop the webhook_url parameter while sending the document. This design ensures autonomous AI agents and automated scripts fail fast with transparent status feedback rather than waiting infinitely for a webhook notification that was never scheduled.
What is Signbee's webhook retry policy if our receiver endpoint is temporarily offline?
Signbee utilizes a predictable 10-second fire-and-forget HTTP POST delivery model without automatic background retry queues. If your receiver endpoint drops the connection, times out, or returns a non-2xx status code, Signbee will not saturate your infrastructure with exponential retry storms. Instead, your workflow should implement a resilient failover mechanism: if your agent has not received a webhook confirmation within a predefined operational window, it can poll GET /api/v1/documents/{id} to inspect the document status and download the certified signed PDF URL directly.
Can markdown GFM tables be rendered when using webhooks?
Yes. Markdown GitHub Flavored Markdown (GFM) pipe tables are fully supported across all Signbee plans, including the Free tier. When your application dispatches a contract containing structured markdown tables (such as pricing schedules, equipment inventories, or payment milestones), Signbee's rendering engine compiles them into crisp vector table layouts inside the certified PDF. The resulting signed document is stamped with SHA-256 integrity hashes and delivered in the document.signed webhook payload without restrictions.
Related Technical Documentation
Related resources
Integrate document.signed into Your AI Agent Stack
Upgrade to Pro or Business: pass your webhook endpoint on send, verify HMAC signatures in constant time, and eliminate polling loops forever.