Skip to content

Designing Reliable Webhooks: Lessons from Production

Enterprise webhooks are a fragmented mess of HMAC, JWT, and thin payloads. Learn how a unified webhook architecture handles verification and automated enrichment.

Sidharth Verma Sidharth Verma · · 6 min read
Designing Reliable Webhooks: Lessons from Production

You pointed a URL at a Lambda function, added an if (req.headers ['x-hub-signature']) check, and called it a day. Now you're three integrations deep, your webhook handler is 800 lines of provider-specific spaghetti, and a silent failure from HiBob just caused a 2:00 AM PagerDuty alert because the payload only contained an employee ID—no actual data.

Webhooks are often marketed as the "simple" way to keep data in sync. In reality, they are the Wild West of software engineering. Every provider—from Salesforce and Slack to HiBob and Jira—has a different "vibe" for how they handle security, payload structures, and retries. If your integration strategy is just pointing a URL at a server and hoping for a valid JSON body, you are building a liability—a common pitfall when building direct integrations in-house.

A unified webhook architecture is a system that centralizes the ingestion, verification, and normalization of asynchronous events from multiple third-party providers into a single, predictable data stream. This architecture eliminates vendor-specific security logic and ensures your application receives enriched, actionable data rather than "thin" notifications.

At Truto, we have processed millions of events across hundreds of SaaS platforms. Here is why the standard approach to webhooks is fundamentally broken and how we engineered a unified engine to fix it.

The Verification Wild West: Solving HMAC, JWT, and Handshakes

Webhook signature verification is the process of cryptographically proving that an incoming HTTP request originated from the expected third-party provider and that the payload has not been tampered with in transit.

There is no "Standard Webhook Security." One service uses HMAC-SHA256, another uses JWT, and a third—like Slack or Microsoft Graph—requires a custom "challenge" handshake before it even starts sending data.

Provider Verification Method What You Need to Implement
GitHub HMAC-SHA256 on raw body Compute hash, compare with X-Hub-Signature-256
Slack Challenge handshake + HMAC Echo challenge value on setup, verify x-slack-signature on events
Microsoft Graph Validation token Return the token as plain text during subscription creation
HiBob Bearer token Compare token in Authorization header with stored secret

The Timing Attack Risk

Most developers compare signatures using a standard string equality operator (==). This is a security failure. Standard string comparison returns as soon as it finds a mismatch, allowing an attacker to use timing analysis to guess the signature byte-by-byte.

Danger

Critical Security Note: Always use constant-time comparison functions like Node's crypto.timingSafeEqual or Web Crypto's crypto.subtle.timingSafeEqual to prevent timing side-channel attacks during webhook verification.

Truto's Unified Verification Engine

We handle this via a declarative verification layer. Instead of writing boilerplate code for every new vendor, we use JSONata expressions to define how to handle challenges and validate signatures. For example, when Slack sends a url_verification event, our engine identifies the challenge field and returns it immediately. Your backend never sees the handshake garbage; it only sees verified, legitimate events.

Solving the "Thin Payload" Problem with Automated Enrichment

Webhook payload enrichment is an architectural pattern where a receiver automatically calls the provider's API to fetch the full resource data after receiving a "thin" notification that only contains an ID.

Most enterprise webhooks are "thin." You get a notification saying employee.updated. Great. What changed? Often, the webhook only gives you a resource_id.

{
  "type": "employee.updated",
  "employee": {
    "id": "2934871"
  }
}

This forces your engineering team to build a manual "fetch-back" loop: receive the webhook, look up the credentials, call the third-party API, handle potential rate limits, and then finally process the data.

The Truto Approach: Automatic Enrichment

When an event hits Truto, our mapping engine—designed to solve the hardest problems in schema normalization—determines if the payload is complete. If it’s thin, the system automatically uses our Unified API to fetch the full, up-to-date resource. This is handled via a method_config in the integration's JSONata mapping, which tells Truto exactly which endpoint to call to retrieve the complete record:

# Example: HiBob mapping triggering automated enrichment
webhooks:
  hibob: |
    (
      $action := $split(body.type,'.')[1];
      body.{
        "event_type": $action = "joined" ? "created" : "updated",
        "resource": "hris/employees",
        "method": "get",
        "method_config": {
          "id" : employee.id
        }
      }
    )

By the time the webhook hits your endpoint, it looks like this:

{
  "id": "3a0da6ba-b2d1-473f-957c-51f6825e3623",
  "event": "integrated_account:created",
  "payload": {
    "id": "79a39d69-e27e-49cb-b9a9-79f5eea7aa26",
    "tenant_id": "acme-1",
    "environment_integration_id": "27a8c0ff-0c2e-4383-b651-0772b3515921",
    "context": {},
    "created_at": "2023-06-16T09:21:21.000Z",
    "updated_at": "2023-06-16T09:21:21.000Z",
    "is_sandbox": false,
    "unified_model_override": {},
    "environment_id": "ac15abdc-b38e-47d0-97a2-69194017c177",
    "integration": {
      "id": "1fa47bf3-5f1f-4b65-bcd0-8d07ab455e15",
      "name": "helpscout",
      "category": "helpdesk",
      "is_beta": false,
      "team_id": "68ea7267-2aec-4da0-b5d9-192cc84eb2de",
      "sharing": "allow",
      "default_oauth_app_id": null,
      "created_at": "2023-02-16T09:27:09.000Z",
      "updated_at": "2023-02-17T12:56:55.000Z"
    },
    "environment_integration": {
      "id": "27a8c0ff-0c2e-4383-b651-0772b3515921",
      "integration_id": "1fa47bf3-5f1f-4b65-bcd0-8d07ab455e15",
      "environment_id": "ac15abdc-b38e-47d0-97a2-69194017c177",
      "show_in_catalog": true,
      "is_enabled": true,
      "override": {},
      "created_at": "2023-02-16T09:27:16.000Z",
      "updated_at": "2023-02-16T09:27:16.000Z"
    }
  },
  "environment_id": "ac15abdc-b38e-47d0-97a2-69194017c177",
  "created_at": "2023-06-16T09:21:22.369Z",
  "webhook_id": "077ec306-5756-43fa-9a06-0cc0da4eabe0"
}

Your system receives a complete, unified record. You don't need to know that HiBob sent a thin event while Workday sent a thick one. The data is already there.

Architecture of a Unified Receiver: Sync vs. Async Fan-out

To handle webhooks at scale, you have to account for two distinct integration architectures: per-account and per-integration.

Feature Per-Account Webhooks Per-Integration (Environment) Webhooks
URL Structure Unique per customer Single URL for all customers
Processing Path Synchronous enrichment Asynchronous queue-based fan-out
Scaling Strategy Direct ingestion context_lookup mapping
Primary Use Case Salesforce, HiBob, HubSpot Slack, Microsoft Teams, Asana

Why the per-integration path needs a queue

When a single webhook could affect dozens of connected accounts, processing them all synchronously would blow past the provider's timeout window. Truto handles this via an asynchronous fan-out path using Cloudflare Queues. We ingest the event, acknowledge it to the vendor within milliseconds, and then move it to a background worker that identifies the relevant integrated accounts and routes the data accordingly.

Best Practices for Webhook Consumers

Whether you use Truto or build your own, follow these three rules to avoid data corruption:

  1. Idempotency is Non-Negotiable: Webhooks are "at-least-once" delivery systems. You will receive the same event twice. Always check if you have already processed an event_id before updating your database.
  2. Verify, Then Process: Never trust a POST request just because it hit your endpoint. Use the X-Truto-Signature to validate the request using constant-time comparison before your business logic runs.
  3. Fast Acknowledgement: Return a 200 OK immediately. If you need to perform a long-running task, put that task in a queue and acknowledge the webhook first. If you take longer than 10 seconds, most vendors will time out and retry, leading to race conditions.

Moving Past Brittle Webhook Logic

Building webhook listeners is easy. Building a unified webhook architecture that handles signature verification, automated enrichment, and reliable delivery across 100+ vendors is an engineering project that takes months and significant capital, often costing upwards of $50,000 per integration to maintain.

By moving the mapping and verification into a declarative, zero-integration-specific code layer, we ensure that when a vendor changes their signature format or payload structure, we patch it in the config—and your code never has to change. The webhook handler you actually want to write looks like this:

app.post('/webhooks/truto', async (req, res) => {
  if (!verifyTrutoSignature(req)) return res.status(401).end();
  
  const { event, payload } = req.body;
  // payload.data contains the full, enriched, unified resource
  await processEvent(event, payload);
  
  res.status(200).json({ ok: true });
});

FAQ

What is a 'thin' webhook payload?
A thin payload is a notification that only contains a resource ID (e.g., an employee ID) rather than the actual changed data. This requires the receiver to make an additional API call to fetch the full record, adding latency and complexity.
How does Truto handle webhook signature verification?
Truto uses a declarative engine to handle formats like HMAC, JWT, and Basic Auth. It manages vendor-specific handshakes (like Slack's challenge) and uses timing-safe comparisons to prevent security vulnerabilities.
Why use R2 storage for webhook payloads?
Standard message queues have size limits (usually 128KB-256KB). By storing large payloads in R2 and passing a pointer through the queue, Truto can deliver massive enterprise datasets without hitting broker limits.
What is webhook fan-out architecture?
Webhook fan-out is used when a single webhook from a provider applies to multiple connected accounts. The receiver acknowledges the webhook immediately, enqueues it for async processing, and then routes the event to all matching accounts.

More from our Blog