Webhooks

Intro

A webhook is an HTTP callback: when an event occurs in a producer system, it sends an HTTP POST with event data to a URL the consumer registered in advance. This inverts the communication direction compared to polling — instead of the consumer repeatedly asking "anything new?", the producer pushes notifications in near real-time. You reach for webhooks when you need low-latency, push-based integration between systems that communicate over HTTP, especially across organizational boundaries where shared message brokers are impractical (payment providers, source control platforms, SaaS integrations).

The mechanism is straightforward: the consumer registers a callback URL with the producer, optionally negotiating a shared secret for signature verification. When the producer detects a relevant event, it serializes a payload (usually JSON), signs it with HMAC-SHA256 using the shared secret, and sends an HTTP POST to the registered URL. The consumer validates the signature, processes the payload, and returns 200 OK to acknowledge receipt. If the producer receives no success response within a timeout, it retries with exponential backoff.

sequenceDiagram
    participant Consumer as Consumer Service
    participant Producer as Producer Service

    Consumer->>Producer: Register callback URL + shared secret
    Note over Producer: Event occurs internally
    Producer->>Consumer: POST /webhook with JSON payload + HMAC signature header
    Consumer->>Consumer: Verify HMAC signature
    Consumer->>Consumer: Check idempotency key and process event
    Consumer-->>Producer: 200 OK
    Note over Producer: If no 2xx within timeout retry with exponential backoff

Webhooks complement Event-Driven Architecture — they are the HTTP-native way to deliver events between systems that do not share a message broker. For internal service-to-service communication within the same platform, Message Queues are usually a better fit because they provide built-in durability, fan-out, and back-pressure.

A production-quality webhook receiver needs three things: signature verification, idempotency, and fast acknowledgment. This example receives GitHub-style webhooks signed with HMAC-SHA256.

using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/webhooks/github", async (
    HttpContext ctx,
    [FromServices] IWebhookProcessor processor,
    CancellationToken ct) =>
{
    // 1. Read raw body (need exact bytes for HMAC verification)
    ctx.Request.EnableBuffering();
    var body = await new StreamReader(ctx.Request.Body).ReadToEndAsync(ct);

    // 2. Verify HMAC-SHA256 signature
    var signature = ctx.Request.Headers["X-Hub-Signature-256"].FirstOrDefault();
    var secret = builder.Configuration["Webhooks:GitHubSecret"]!;

    if (!VerifySignature(body, signature, secret))
        return Results.Unauthorized();

    // 3. Extract idempotency key and event type
    var deliveryId = ctx.Request.Headers["X-GitHub-Delivery"].FirstOrDefault();
    var eventType = ctx.Request.Headers["X-GitHub-Event"].FirstOrDefault();

    if (string.IsNullOrEmpty(deliveryId) || string.IsNullOrEmpty(eventType))
        return Results.BadRequest();

    // 4. Acknowledge fast, process async
    //    In production: enqueue to durable storage, return 200 immediately
    await processor.EnqueueAsync(eventType, deliveryId, body, ct);

    return Results.Ok();
});

app.Run();

static bool VerifySignature(string payload, string? signatureHeader, string secret)
{
    if (string.IsNullOrEmpty(signatureHeader) || !signatureHeader.StartsWith("sha256="))
        return false;

    var expected = signatureHeader["sha256=".Length..];
    var keyBytes = Encoding.UTF8.GetBytes(secret);
    var payloadBytes = Encoding.UTF8.GetBytes(payload);

    var hash = HMACSHA256.HashData(keyBytes, payloadBytes);
    var computed = Convert.ToHexStringLower(hash);

    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(computed),
        Encoding.UTF8.GetBytes(expected));
}

Key implementation decisions:

Tradeoffs: Webhooks vs Polling vs SSE vs WebSockets

Approach Direction Latency Complexity Connection Best fit
Webhooks Push (server to server) Near real-time Moderate (retry, signatures, idempotency) Per-event HTTP request Cross-org integrations, SaaS event delivery
Polling Pull (consumer to producer) Interval-bound delay Low Stateless per-request Simple integrations, systems without webhook support
SSE Push (server to browser) Real-time Low Long-lived HTTP stream One-directional browser notifications, dashboards
WebSockets Bidirectional Real-time High (connection management, scaling) Persistent TCP Chat, collaborative editing, real-time bidirectional flows

Decision heuristic:

Pitfalls

1) At-Least-Once Delivery Without Idempotency

2) Slow Processing Causes Timeout and Retry Storm

3) Missing or Weak Signature Verification

4) Endpoint Availability and Missed Events

Questions

References


Whats next