A signed document sending a gold signal into a black cube
Agent Systems ArchitectureUpdated September 2026 · 12 min read

How to Send a Signing Packet from an Agent With Webhooks

When autonomous AI agents generate contracts, they cannot afford to block serverless threads or poll HTTP endpoints indefinitely while waiting for human signers. Integrating asynchronous webhooks allows your agent systems to dispatch agreements in milliseconds, sleep, and re-awaken the exact moment human counterparties sign. Here is the complete engineering guide to dispatching, HMAC verification, and resilient event routing.

Michael Beckett
Michael Beckett

Founder, Signbee

1 Event

document.signed

10s

HTTP Timeout

HMAC-256

Payload Security

Fallback

REST Poll API

Architectural Summary (TL;DR)

Pro and Business tiers: attach webhook_url to your initial POST /api/v1/send payload. The 200 response returns a per-user webhook_secret. When the signer executes the contract in their browser, Signbee pushes an authenticated document.signed event to your endpoint. Verify the HMAC-SHA256 signature using constant-time comparison. If your agent runs behind a NAT firewall or in a local CLI without ingress, use our polling fallback workflow.

Push vs Pull Architecture: Webhooks vs Polling Comparison

When designing agentic contract workflows, developers must choose between reactive push (webhooks) and active pull (polling):

Architecture VectorReactive Push (Webhooks)Active Pull (Polling)
Notification Latency< 150ms (Immediate push)Bounded by polling interval (e.g. 5–60 min)
Compute ConsumptionZero idle compute (Serverless wakes on POST)Continuous CPU cycles spent polling
Ingress RequirementPublic HTTPS URL requiredNone (Runs behind NAT, firewall, laptop)
Best Used ForCloud backends, microservices, Slack botsLocal CLI agents, desktop IDEs (Cursor/Claude)

Dispatching Contracts with Webhook Configuration

To enable push callbacks, pass webhook_url in your dispatch request. The response immediately delivers an immutable webhook_secret used to authenticate incoming events:

Bash / curl — Dispatch with Webhook
curl -X POST https://signb.ee/api/v1/send \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "markdown": "# Mutual Non-Disclosure Agreement\n\nTerms...",
    "sender_name": "Autonomous Operations Corp",
    "sender_email": "ops@auto-corp.io",
    "recipient_name": "Bob Smith",
    "recipient_email": "bob@acme.com",
    "webhook_url": "https://api.auto-corp.io/v1/webhooks/signbee"
  }'
HTTP 200 OK Response
{
  "document_id": "doc_8f91a2bc4e",
  "status": "pending_recipient",
  "sender": "Autonomous Operations Corp",
  "recipient": "Bob Smith",
  "expires_at": "2026-09-18T12:00:00.000Z",
  "webhook_secret": "whsec_9a8b7c6d5e4f3a2b1c0d..."
}

Note: Webhooks require a Pro or Business plan. The Free developer tier returns an HTTP 403 error if a webhook URL is passed.

The Webhook Event Specification

When the counterparty completes signing, Signbee delivers an HTTP POST request with the following headers and payload:

Content-Type: application/json

X-Signbee-Signature: 3a8b4c7d9e... (HMAC-SHA256 hex digest of raw request body)

X-Signbee-Event: document.signed

User-Agent: Signbee-Webhook/1.0

document.signed Webhook Payload
{
  "event": "document.signed",
  "document_id": "doc_8f91a2bc4e",
  "title": "Mutual Non-Disclosure Agreement",
  "status": "signed",
  "sender_email": "ops@auto-corp.io",
  "recipient_name": "Bob Smith",
  "recipient_email": "bob@acme.com",
  "recipient_signed_at": "2026-09-04T12:34:56.000Z",
  "signed_pdf_url": "https://signb.ee/uploads/signed_doc_8f91a2bc4e.pdf",
  "signature_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "verify_url": "https://signb.ee/verify/doc_8f91a2bc4e",
  "timestamp": "2026-09-04T12:34:57.000Z"
}

Timing-Safe HMAC Verification Implementations

Always verify the incoming digest against the raw binary payload before parsing JSON. Here are production snippets in Node.js and Python:

TypeScript / Node.js (Express / Next.js)
import crypto from "crypto";

export function verifyWebhookSignature(
  rawPayload: string | Buffer,
  signatureHeader: string,
  webhookSecret: string
): boolean {
  if (!signatureHeader || !webhookSecret) return false;

  const computedHash = crypto
    .createHmac("sha256", webhookSecret)
    .update(rawPayload)
    .digest("hex");

  const expectedBuffer = Buffer.from(computedHash, "utf8");
  const signatureBuffer = Buffer.from(signatureHeader, "utf8");

  if (expectedBuffer.length !== signatureBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
}
Python (FastAPI / Flask)
import hmac
import hashlib

def verify_signbee_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    """Performs constant-time HMAC-SHA256 validation to prevent timing attacks."""
    if not signature_header or not secret:
        return False

    computed = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(computed.lower(), signature_header.lower())

Complete Production Next.js App Router Route Handler

In production serverless runtimes, an inbound webhook endpoint must never perform heavy work (like PDF downloading, OCR analysis, or CRM updates) synchronously inside the HTTP handler. Doing so risks exceeding cloud function timeouts. The golden architecture returns an HTTP 200 within 20 milliseconds and enqueues the payload into a background job queue:

app/api/webhooks/signbee/route.ts (Next.js 15+ App Router)
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { inngest } from "@/lib/inngest/client"; // Or Upstash QStash / BullMQ

export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  try {
    const rawBody = await req.text();
    const signature = req.headers.get("x-signbee-signature");
    const secret = process.env.SIGNBEE_WEBHOOK_SECRET;

    if (!signature || !secret) {
      return NextResponse.json({ error: "Missing signature credentials" }, { status: 401 });
    }

    // Step 1: Constant-time HMAC verification
    const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    const a = Buffer.from(expected, "utf8");
    const b = Buffer.from(signature, "utf8");

    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return NextResponse.json({ error: "Invalid cryptographic signature" }, { status: 403 });
    }

    const event = JSON.parse(rawBody);

    // Step 2: Prevent replay attacks (reject payloads older than 5 minutes)
    const eventTime = new Date(event.timestamp).getTime();
    if (Math.abs(Date.now() - eventTime) > 300_000) {
      return NextResponse.json({ error: "Stale event timestamp rejected" }, { status: 400 });
    }

    // Step 3: Enqueue for asynchronous background agent execution
    await inngest.send({
      name: "contract.signed",
      data: {
        documentId: event.document_id,
        pdfUrl: event.signed_pdf_url,
        signatureHash: event.signature_hash,
        recipientEmail: event.recipient_email,
        signedAt: event.recipient_signed_at,
      },
    });

    // Step 4: Acknowledge receipt immediately
    return NextResponse.json({ received: true, id: event.document_id });
  } catch (err: any) {
    console.error("[Webhook Error]:", err.message);
    return NextResponse.json({ error: "Internal processing failure" }, { status: 500 });
  }
}

Local Development: Testing Agent Webhooks Behind NAT Firewalls

When developing autonomous agents on localhost or inside sandboxed dev containers, your machine lacks a publicly routable IPv4 address. You can forward live Signbee webhooks directly to your local workstation using Cloudflare Tunnel or ngrok:

Exposing Local Agent Listener via Cloudflare Tunnel
# 1. Install and start a zero-config tunnel to your local port 3000
npx cloudflared tunnel --url http://localhost:3000

# Console Output:
# +--------------------------------------------------------------------------------------------+
# |  Your quick Tunnel has been created! Visit it at (it may take some moments to be ready):  |
# |  https://temporary-agent-subdomain.trycloudflare.com                                       |
# +--------------------------------------------------------------------------------------------+

Supply the resulting public URL as your webhook_url when calling POST /api/v1/send during testing. Your local development server will receive real-time webhooks the moment a test contract is completed.

Handling Missing Webhooks: The Resilient Failover Loop

Because network partitions and server deployments happen, mission-critical agent workflows must combine webhooks with a circuit-breaker polling failover:

TypeScript — Resilient Webhook + Failover Poller
async function waitForSignature(documentId: string, timeoutMinutes = 60): Promise<boolean> {
  const startTime = Date.now();
  const maxTime = startTime + timeoutMinutes * 60 * 1000;

  // Poll fallback loop running every 5 minutes if webhook has not arrived
  while (Date.now() < maxTime) {
    await new Promise((r) => setTimeout(r, 300_000)); // 5 min interval

    const res = await fetch(`https://signb.ee/api/v1/documents/${documentId}`, {
      headers: { "Authorization": `Bearer ${process.env.SIGNBEE_API_KEY}` }
    });

    if (res.ok) {
      const data = await res.json();
      if (data.status === "signed") {
        console.log(`Document ${documentId} signed via fallback polling reconciliation!`);
        return true;
      }
    }
  }

  return false;
}

Frequently Asked Questions

Which webhook event does Signbee emit upon signature completion?

Signbee follows a strict philosophy of event minimalism, emitting exactly one primary webhook event: document.signed. This event fires the exact millisecond all required counterparties complete their signature ceremonies and the cryptographic SHA-256 Certificate of Completion has been generated. The JSON payload delivers the immutable document_id, verified signer metadata, UTC completion timestamps, a secure download URL for the finalized PDF, and the cryptographic hash digest. No intermediate noise events (such as document.viewed or email.delivered) are emitted, keeping agent listener implementations lean and deterministic.

How should an agent handle webhooks without an inbound public URL?

AI agents executing inside local sandboxes, developer terminals, or desktop IDE environments (like Cursor or Claude Desktop) lack public inbound HTTP interfaces. In this topology, developers use a hybrid architecture: the agent dispatches the contract via REST or MCP and persists the document_id to a database or state file. A central cloud-hosted webhook listener endpoint receives the document.signed POST, validates the HMAC-SHA256 signature, and flags the contract as complete. Alternatively, local agents can either use a reverse proxy tunnel (such as ngrok or Cloudflare Tunnel) or execute an exponential backoff polling loop against GET /api/v1/documents/{id}.

Why is timing-safe HMAC-SHA256 verification mandatory?

Timing attacks allow malicious actors to guess secret cryptographic keys character by character by measuring microscopic differences in how long string comparison operations take. Using standard equality operators (like == or ===) terminates evaluation at the first non-matching character, leaking timing telemetry. To guarantee security, webhook route handlers must compute the HMAC-SHA256 digest of the raw, unparsed request body using their per-user webhook_secret and execute a constant-time comparison using crypto.timingSafeEqual() in Node.js or hmac.compare_digest() in Python.

Does Signbee retry webhook deliveries upon failure?

Signbee operates on an ultra-fast, fire-and-forget delivery model with a strict 10-second HTTP timeout per dispatch and does not execute automatic exponential retry storms. This design prevents cascading denial-of-service failures on customer servers. If your webhook listener drops offline, restarts during deployment, or experiences a network partition, your application must implement a safety failover polling mechanism: query GET /api/v1/documents/{id} on a low-frequency cron or heartbeat to reconcile any missed completion events.

Build Event-Driven Agent Contract Workflows

Connect your AI agents to real-time e-signatures. Pro and Business tiers include webhook callbacks.