Home/Blog/Building a Webhook Provider: Design, Delivery, Documentation & SDK Guide

Building a Webhook Provider: Design, Delivery, Documentation & SDK Guide

Learn to build production-grade webhook delivery systems. Master webhook API design, reliable delivery infrastructure, signature verification, retry logic, documentation standards, and client SDK development.

By Inventive Software Engineering
Building a Webhook Provider: Design, Delivery, Documentation & SDK Guide

Building a webhook system that other developers will integrate with requires careful API design, reliable infrastructure, comprehensive documentation, and developer-friendly SDKs. This guide covers everything you need to build a production-grade webhook provider from the ground up.

Webhook Provider Architecture

┌──────────────────────────────────────────────────────────────────────────────┐
│                    WEBHOOK PROVIDER ARCHITECTURE                              │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   Your Application                                                           │
│   ─────────────────                                                          │
│   ┌──────────────┐                                                          │
│   │ Event Source │  (User actions, system events, scheduled jobs)            │
│   └──────┬───────┘                                                          │
│          │                                                                   │
│          ▼                                                                   │
│   ┌──────────────┐     ┌──────────────┐     ┌──────────────┐               │
│   │   Event      │────▶│   Webhook    │────▶│   Delivery   │               │
│   │   Emitter    │     │   Queue      │     │   Workers    │               │
│   └──────────────┘     └──────────────┘     └──────┬───────┘               │
│                                                     │                        │
│                                                     │                        │
│   ┌─────────────────────────────────────────────────┼────────────────────┐  │
│   │                    Delivery Pipeline            │                    │  │
│   │                    ─────────────────            │                    │  │
│   │                                                 ▼                    │  │
│   │  ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐         │  │
│   │  │ Lookup   │──▶│  Sign    │──▶│  Send    │──▶│  Record  │         │  │
│   │  │ Endpoint │   │  Payload │   │  Request │   │  Result  │         │  │
│   │  └──────────┘   └──────────┘   └──────────┘   └──────────┘         │  │
│   │                                      │                               │  │
│   │                                      ▼                               │  │
│   │                              ┌──────────────┐                        │  │
│   │                              │ Retry Queue  │                        │  │
│   │                              │ (on failure) │                        │  │
│   │                              └──────────────┘                        │  │
│   └──────────────────────────────────────────────────────────────────────┘  │
│                                                                              │
│   Customer Endpoints                                                         │
│   ──────────────────                                                         │
│   ┌──────────────┐  ┌──────────────┐  ┌──────────────┐                     │
│   │  Customer A  │  │  Customer B  │  │  Customer C  │                     │
│   │  Endpoint    │  │  Endpoint    │  │  Endpoint    │                     │
│   └──────────────┘  └──────────────┘  └──────────────┘                     │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

Event Model Design

Standard Event Envelope

// event-types.ts
interface WebhookEvent<T = any> {
  // Unique identifier for this event
  id: string;

  // Event type using dot notation
  type: string;

  // API version for payload schema
  api_version: string;

  // When the event was created
  created_at: string; // ISO 8601

  // The actual event data
  data: {
    // The primary object that triggered the event
    object: T;

    // For update events, include previous values
    previous_attributes?: Partial<T>;
  };

  // Indicates if this is from a live or test environment
  livemode: boolean;

  // For webhook deliveries
  webhook_metadata?: {
    delivery_id: string;
    attempt: number;
    endpoint_id: string;
  };
}

// Example event types
type OrderCreatedEvent = WebhookEvent<{
  id: string;
  customer_id: string;
  total: number;
  currency: string;
  status: 'pending';
  items: OrderItem[];
  created_at: string;
}>;

type OrderUpdatedEvent = WebhookEvent<{
  id: string;
  status: 'processing' | 'shipped' | 'delivered';
  updated_at: string;
}>;

Event Type Naming Convention

// event-catalog.ts
export const EventTypes = {
  // Resource lifecycle events
  'order.created': 'When a new order is placed',
  'order.updated': 'When an order is modified',
  'order.paid': 'When payment is confirmed',
  'order.fulfilled': 'When order is shipped',
  'order.cancelled': 'When order is cancelled',
  'order.refunded': 'When order is refunded',

  'customer.created': 'When a new customer registers',
  'customer.updated': 'When customer info changes',
  'customer.deleted': 'When customer account is deleted',

  'subscription.created': 'When subscription starts',
  'subscription.updated': 'When subscription changes',
  'subscription.cancelled': 'When subscription is cancelled',
  'subscription.renewed': 'When subscription auto-renews',

  'invoice.created': 'When invoice is generated',
  'invoice.paid': 'When invoice is paid',
  'invoice.payment_failed': 'When payment attempt fails',

  // System events
  'ping': 'Test event for endpoint verification',
} as const;

export type EventType = keyof typeof EventTypes;

Creating Events

// event-emitter.ts
import { nanoid } from 'nanoid';
import { webhookQueue } from './queue';

class EventEmitter {
  private apiVersion = '2024-01-15';

  async emit<T>(
    type: EventType,
    data: T,
    options: {
      previousAttributes?: Partial<T>;
      livemode?: boolean;
      customerId?: string;
    } = {}
  ): Promise<string> {
    const eventId = `evt_${nanoid(24)}`;

    const event: WebhookEvent<T> = {
      id: eventId,
      type,
      api_version: this.apiVersion,
      created_at: new Date().toISOString(),
      data: {
        object: data,
        ...(options.previousAttributes && {
          previous_attributes: options.previousAttributes
        })
      },
      livemode: options.livemode ?? process.env.NODE_ENV === 'production'
    };

    // Store event for replay capability
    await this.storeEvent(event);

    // Queue for delivery
    await this.queueDeliveries(event, options.customerId);

    return eventId;
  }

  private async storeEvent(event: WebhookEvent): Promise<void> {
    await db.query(`
      INSERT INTO webhook_events (id, type, api_version, data, created_at)
      VALUES ($1, $2, $3, $4, $5)
    `, [event.id, event.type, event.api_version, JSON.stringify(event.data), event.created_at]);
  }

  private async queueDeliveries(
    event: WebhookEvent,
    customerId?: string
  ): Promise<void> {
    // Find all endpoints subscribed to this event type
    const endpoints = await this.getSubscribedEndpoints(event.type, customerId);

    for (const endpoint of endpoints) {
      await webhookQueue.add('delivery', {
        eventId: event.id,
        endpointId: endpoint.id,
        url: endpoint.url,
        secret: endpoint.secret,
        event
      }, {
        attempts: 5,
        backoff: {
          type: 'exponential',
          delay: 60000 // Start at 1 minute
        }
      });
    }
  }

  private async getSubscribedEndpoints(
    eventType: string,
    customerId?: string
  ): Promise<WebhookEndpoint[]> {
    let query = `
      SELECT * FROM webhook_endpoints
      WHERE enabled = true
        AND (subscribed_events @> $1 OR subscribed_events @> '["*"]')
    `;
    const params: any[] = [JSON.stringify([eventType])];

    if (customerId) {
      query += ` AND customer_id = $2`;
      params.push(customerId);
    }

    const result = await db.query(query, params);
    return result.rows;
  }
}

export const eventEmitter = new EventEmitter();

Delivery Infrastructure

Webhook Delivery Worker

// delivery-worker.ts
import { Worker, Job } from 'bullmq';
import crypto from 'crypto';
import { metrics } from './metrics';

interface DeliveryJob {
  eventId: string;
  endpointId: string;
  url: string;
  secret: string;
  event: WebhookEvent;
}

const deliveryWorker = new Worker<DeliveryJob>(
  'webhook-delivery',
  async (job: Job<DeliveryJob>) => {
    const { eventId, endpointId, url, secret, event } = job.data;
    const deliveryId = `del_${nanoid(24)}`;
    const attempt = job.attemptsMade + 1;

    // Add delivery metadata
    const eventWithMetadata = {
      ...event,
      webhook_metadata: {
        delivery_id: deliveryId,
        attempt,
        endpoint_id: endpointId
      }
    };

    // Sign the payload
    const timestamp = Math.floor(Date.now() / 1000);
    const payload = JSON.stringify(eventWithMetadata);
    const signature = signPayload(payload, timestamp, secret);

    const startTime = Date.now();

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'User-Agent': 'YourApp-Webhooks/1.0',
          'X-Webhook-ID': deliveryId,
          'X-Webhook-Timestamp': timestamp.toString(),
          'X-Webhook-Signature': signature,
          'X-Webhook-Event': event.type
        },
        body: payload,
        signal: AbortSignal.timeout(30000) // 30 second timeout
      });

      const duration = Date.now() - startTime;
      const responseBody = await response.text().catch(() => '');

      // Log delivery attempt
      await logDeliveryAttempt({
        deliveryId,
        eventId,
        endpointId,
        attempt,
        url,
        statusCode: response.status,
        responseBody: responseBody.slice(0, 1000),
        duration,
        success: response.ok
      });

      // Update metrics
      metrics.deliveryDuration.observe({ endpoint_id: endpointId }, duration / 1000);

      if (!response.ok) {
        metrics.deliveryFailures.inc({
          endpoint_id: endpointId,
          status_code: response.status
        });

        // Don't retry on client errors (except 429)
        if (response.status >= 400 && response.status < 500 && response.status !== 429) {
          // Log but don't retry
          return {
            success: false,
            statusCode: response.status,
            noRetry: true
          };
        }

        throw new Error(`HTTP ${response.status}: ${responseBody.slice(0, 200)}`);
      }

      metrics.deliverySuccesses.inc({ endpoint_id: endpointId });

      return {
        success: true,
        statusCode: response.status,
        duration
      };

    } catch (error: any) {
      const duration = Date.now() - startTime;

      await logDeliveryAttempt({
        deliveryId,
        eventId,
        endpointId,
        attempt,
        url,
        statusCode: 0,
        error: error.message,
        duration,
        success: false
      });

      // Check if endpoint is consistently failing
      await checkEndpointHealth(endpointId);

      throw error;
    }
  },
  {
    concurrency: 50,
    limiter: {
      max: 100,
      duration: 1000
    }
  }
);

function signPayload(payload: string, timestamp: number, secret: string): string {
  const signedPayload = `${timestamp}.${payload}`;
  const signature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  return `t=${timestamp},v1=${signature}`;
}

Endpoint Health Management

// endpoint-health.ts
interface EndpointHealth {
  endpointId: string;
  consecutiveFailures: number;
  lastSuccess: Date | null;
  lastFailure: Date | null;
  status: 'healthy' | 'degraded' | 'disabled';
}

async function checkEndpointHealth(endpointId: string): Promise<void> {
  const recentDeliveries = await db.query(`
    SELECT success, created_at
    FROM webhook_deliveries
    WHERE endpoint_id = $1
    ORDER BY created_at DESC
    LIMIT 10
  `, [endpointId]);

  const failures = recentDeliveries.rows.filter(d => !d.success);

  if (failures.length >= 10) {
    // All recent deliveries failed - disable endpoint
    await disableEndpoint(endpointId, 'Consecutive delivery failures');
  } else if (failures.length >= 5) {
    // Many failures - mark as degraded
    await markEndpointDegraded(endpointId);
  }
}

async function disableEndpoint(endpointId: string, reason: string): Promise<void> {
  await db.query(`
    UPDATE webhook_endpoints
    SET enabled = false, disabled_reason = $2, disabled_at = NOW()
    WHERE id = $1
  `, [endpointId, reason]);

  // Notify the customer
  const endpoint = await getEndpoint(endpointId);
  await sendEndpointDisabledNotification(endpoint.customer_id, {
    endpointId,
    url: endpoint.url,
    reason
  });
}

async function markEndpointDegraded(endpointId: string): Promise<void> {
  await db.query(`
    UPDATE webhook_endpoints
    SET status = 'degraded', degraded_at = NOW()
    WHERE id = $1
  `, [endpointId]);
}

Webhook Registration API

Endpoint Management

// webhook-api.ts
import express from 'express';
import { nanoid } from 'nanoid';
import crypto from 'crypto';

const router = express.Router();

// List webhook endpoints
router.get('/webhook-endpoints', async (req, res) => {
  const customerId = req.auth.customerId;

  const endpoints = await db.query(`
    SELECT id, url, description, subscribed_events, enabled, status,
           created_at, disabled_reason
    FROM webhook_endpoints
    WHERE customer_id = $1
    ORDER BY created_at DESC
  `, [customerId]);

  res.json({ data: endpoints.rows });
});

// Create webhook endpoint
router.post('/webhook-endpoints', async (req, res) => {
  const customerId = req.auth.customerId;
  const { url, description, events } = req.body;

  // Validate URL
  if (!isValidWebhookUrl(url)) {
    return res.status(400).json({
      error: {
        code: 'invalid_url',
        message: 'URL must be HTTPS and publicly accessible'
      }
    });
  }

  // Validate events
  const validEvents = events.filter((e: string) =>
    e === '*' || EventTypes[e as EventType]
  );

  if (validEvents.length === 0) {
    return res.status(400).json({
      error: {
        code: 'invalid_events',
        message: 'At least one valid event type required'
      }
    });
  }

  // Generate endpoint ID and secret
  const endpointId = `we_${nanoid(24)}`;
  const secret = `whsec_${crypto.randomBytes(32).toString('base64url')}`;

  await db.query(`
    INSERT INTO webhook_endpoints (
      id, customer_id, url, description, secret,
      subscribed_events, enabled, status
    ) VALUES ($1, $2, $3, $4, $5, $6, true, 'healthy')
  `, [endpointId, customerId, url, description, secret, JSON.stringify(validEvents)]);

  // Send test ping
  await sendTestPing(endpointId, url, secret);

  res.status(201).json({
    data: {
      id: endpointId,
      url,
      description,
      subscribed_events: validEvents,
      secret, // Only shown once!
      enabled: true,
      status: 'healthy',
      created_at: new Date().toISOString()
    }
  });
});

// Update webhook endpoint
router.patch('/webhook-endpoints/:id', async (req, res) => {
  const customerId = req.auth.customerId;
  const endpointId = req.params.id;
  const { url, description, events, enabled } = req.body;

  // Verify ownership
  const endpoint = await getEndpointIfOwned(endpointId, customerId);
  if (!endpoint) {
    return res.status(404).json({ error: { code: 'not_found' } });
  }

  const updates: string[] = [];
  const values: any[] = [];
  let paramIndex = 1;

  if (url !== undefined) {
    if (!isValidWebhookUrl(url)) {
      return res.status(400).json({
        error: { code: 'invalid_url' }
      });
    }
    updates.push(`url = $${paramIndex++}`);
    values.push(url);
  }

  if (description !== undefined) {
    updates.push(`description = $${paramIndex++}`);
    values.push(description);
  }

  if (events !== undefined) {
    updates.push(`subscribed_events = $${paramIndex++}`);
    values.push(JSON.stringify(events));
  }

  if (enabled !== undefined) {
    updates.push(`enabled = $${paramIndex++}`);
    values.push(enabled);
    if (enabled) {
      updates.push(`disabled_reason = NULL, disabled_at = NULL`);
      updates.push(`status = 'healthy'`);
    }
  }

  if (updates.length === 0) {
    return res.status(400).json({
      error: { code: 'no_updates' }
    });
  }

  values.push(endpointId);
  await db.query(`
    UPDATE webhook_endpoints
    SET ${updates.join(', ')}, updated_at = NOW()
    WHERE id = $${paramIndex}
  `, values);

  const updated = await getEndpoint(endpointId);
  res.json({ data: updated });
});

// Delete webhook endpoint
router.delete('/webhook-endpoints/:id', async (req, res) => {
  const customerId = req.auth.customerId;
  const endpointId = req.params.id;

  const endpoint = await getEndpointIfOwned(endpointId, customerId);
  if (!endpoint) {
    return res.status(404).json({ error: { code: 'not_found' } });
  }

  await db.query('DELETE FROM webhook_endpoints WHERE id = $1', [endpointId]);

  res.status(204).send();
});

// Rotate secret
router.post('/webhook-endpoints/:id/rotate-secret', async (req, res) => {
  const customerId = req.auth.customerId;
  const endpointId = req.params.id;

  const endpoint = await getEndpointIfOwned(endpointId, customerId);
  if (!endpoint) {
    return res.status(404).json({ error: { code: 'not_found' } });
  }

  const newSecret = `whsec_${crypto.randomBytes(32).toString('base64url')}`;

  // Keep old secret valid for 24 hours
  await db.query(`
    UPDATE webhook_endpoints
    SET secret = $1,
        previous_secret = secret,
        previous_secret_expires_at = NOW() + INTERVAL '24 hours'
    WHERE id = $2
  `, [newSecret, endpointId]);

  res.json({
    data: {
      secret: newSecret,
      previous_secret_expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
    }
  });
});

export default router;

Delivery History API

// delivery-history-api.ts
router.get('/webhook-endpoints/:id/deliveries', async (req, res) => {
  const customerId = req.auth.customerId;
  const endpointId = req.params.id;

  const endpoint = await getEndpointIfOwned(endpointId, customerId);
  if (!endpoint) {
    return res.status(404).json({ error: { code: 'not_found' } });
  }

  const { limit = 20, starting_after, event_type, status } = req.query;

  let query = `
    SELECT
      d.id as delivery_id,
      d.event_id,
      e.type as event_type,
      d.attempt,
      d.status_code,
      d.success,
      d.duration_ms,
      d.error,
      d.created_at
    FROM webhook_deliveries d
    JOIN webhook_events e ON d.event_id = e.id
    WHERE d.endpoint_id = $1
  `;
  const params: any[] = [endpointId];
  let paramIndex = 2;

  if (starting_after) {
    query += ` AND d.created_at < (SELECT created_at FROM webhook_deliveries WHERE id = $${paramIndex++})`;
    params.push(starting_after);
  }

  if (event_type) {
    query += ` AND e.type = $${paramIndex++}`;
    params.push(event_type);
  }

  if (status === 'success') {
    query += ` AND d.success = true`;
  } else if (status === 'failed') {
    query += ` AND d.success = false`;
  }

  query += ` ORDER BY d.created_at DESC LIMIT $${paramIndex}`;
  params.push(parseInt(limit as string) + 1);

  const result = await db.query(query, params);
  const deliveries = result.rows.slice(0, parseInt(limit as string));
  const hasMore = result.rows.length > parseInt(limit as string);

  res.json({
    data: deliveries,
    has_more: hasMore
  });
});

// Retry a failed delivery
router.post('/webhook-endpoints/:id/deliveries/:deliveryId/retry', async (req, res) => {
  const customerId = req.auth.customerId;
  const { id: endpointId, deliveryId } = req.params;

  const endpoint = await getEndpointIfOwned(endpointId, customerId);
  if (!endpoint) {
    return res.status(404).json({ error: { code: 'not_found' } });
  }

  // Get the original event
  const delivery = await db.query(`
    SELECT d.*, e.data as event_data, e.type as event_type
    FROM webhook_deliveries d
    JOIN webhook_events e ON d.event_id = e.id
    WHERE d.id = $1 AND d.endpoint_id = $2
  `, [deliveryId, endpointId]);

  if (!delivery.rows[0]) {
    return res.status(404).json({ error: { code: 'delivery_not_found' } });
  }

  // Queue for retry
  await webhookQueue.add('delivery', {
    eventId: delivery.rows[0].event_id,
    endpointId,
    url: endpoint.url,
    secret: endpoint.secret,
    event: {
      id: delivery.rows[0].event_id,
      type: delivery.rows[0].event_type,
      data: delivery.rows[0].event_data
    },
    manualRetry: true
  });

  res.json({
    data: {
      message: 'Delivery queued for retry'
    }
  });
});

Signature Verification SDKs

Node.js SDK

// sdk/node/src/index.ts
import crypto from 'crypto';

export interface WebhookEvent {
  id: string;
  type: string;
  api_version: string;
  created_at: string;
  data: {
    object: any;
    previous_attributes?: any;
  };
  livemode: boolean;
}

export interface VerifyOptions {
  tolerance?: number; // Timestamp tolerance in seconds
}

export class WebhookSignatureError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'WebhookSignatureError';
  }
}

export function constructEvent(
  payload: string | Buffer,
  signatureHeader: string,
  secret: string,
  options: VerifyOptions = {}
): WebhookEvent {
  const tolerance = options.tolerance ?? 300; // 5 minutes default

  // Parse signature header
  const parts = signatureHeader.split(',');
  const timestamp = parts.find(p => p.startsWith('t='))?.slice(2);
  const signatures = parts
    .filter(p => p.startsWith('v1='))
    .map(p => p.slice(3));

  if (!timestamp || signatures.length === 0) {
    throw new WebhookSignatureError('Invalid signature format');
  }

  // Check timestamp
  const timestampNum = parseInt(timestamp, 10);
  const now = Math.floor(Date.now() / 1000);

  if (Math.abs(now - timestampNum) > tolerance) {
    throw new WebhookSignatureError(
      `Timestamp outside tolerance. Event timestamp: ${timestampNum}, current: ${now}`
    );
  }

  // Verify signature
  const payloadString = typeof payload === 'string' ? payload : payload.toString('utf8');
  const signedPayload = `${timestamp}.${payloadString}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  const signatureValid = signatures.some(sig => {
    try {
      return crypto.timingSafeEqual(
        Buffer.from(sig, 'hex'),
        Buffer.from(expectedSignature, 'hex')
      );
    } catch {
      return false;
    }
  });

  if (!signatureValid) {
    throw new WebhookSignatureError('Invalid signature');
  }

  // Parse and return event
  try {
    return JSON.parse(payloadString);
  } catch {
    throw new WebhookSignatureError('Invalid JSON payload');
  }
}

// Express middleware
export function webhookMiddleware(secret: string, options?: VerifyOptions) {
  return (req: any, res: any, next: any) => {
    const signature = req.headers['x-webhook-signature'];

    if (!signature) {
      return res.status(400).json({ error: 'Missing signature header' });
    }

    try {
      req.webhookEvent = constructEvent(req.body, signature, secret, options);
      next();
    } catch (error) {
      if (error instanceof WebhookSignatureError) {
        return res.status(401).json({ error: error.message });
      }
      throw error;
    }
  };
}

Python SDK

# sdk/python/yourapp/webhooks.py
import hmac
import hashlib
import time
import json
from typing import Any, Optional
from dataclasses import dataclass

class WebhookSignatureError(Exception):
    """Raised when webhook signature verification fails."""
    pass

@dataclass
class WebhookEvent:
    id: str
    type: str
    api_version: str
    created_at: str
    data: dict
    livemode: bool

def construct_event(
    payload: bytes | str,
    signature_header: str,
    secret: str,
    tolerance: int = 300
) -> WebhookEvent:
    """
    Verify webhook signature and return the event.

    Args:
        payload: Raw request body
        signature_header: Value of X-Webhook-Signature header
        secret: Your webhook endpoint secret
        tolerance: Maximum age of event in seconds (default: 300)

    Returns:
        WebhookEvent object

    Raises:
        WebhookSignatureError: If signature verification fails
    """
    if isinstance(payload, str):
        payload = payload.encode('utf-8')

    # Parse signature header
    parts = dict(p.split('=', 1) for p in signature_header.split(',') if '=' in p)

    timestamp = parts.get('t')
    signature = parts.get('v1')

    if not timestamp or not signature:
        raise WebhookSignatureError("Invalid signature format")

    # Check timestamp
    try:
        timestamp_int = int(timestamp)
    except ValueError:
        raise WebhookSignatureError("Invalid timestamp")

    now = int(time.time())
    if abs(now - timestamp_int) > tolerance:
        raise WebhookSignatureError(
            f"Timestamp outside tolerance. Event: {timestamp_int}, Current: {now}"
        )

    # Verify signature
    signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_signature):
        raise WebhookSignatureError("Invalid signature")

    # Parse and return event
    try:
        data = json.loads(payload)
        return WebhookEvent(
            id=data['id'],
            type=data['type'],
            api_version=data['api_version'],
            created_at=data['created_at'],
            data=data['data'],
            livemode=data.get('livemode', True)
        )
    except (json.JSONDecodeError, KeyError) as e:
        raise WebhookSignatureError(f"Invalid payload: {e}")


# Flask decorator
def webhook_handler(secret: str, tolerance: int = 300):
    """
    Flask decorator for webhook handlers.

    Usage:
        @app.route('/webhooks', methods=['POST'])
        @webhook_handler(secret='whsec_...')
        def handle_webhook(event):
            if event.type == 'order.created':
                # Handle order
                pass
            return {'received': True}
    """
    from functools import wraps
    from flask import request, jsonify

    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            signature = request.headers.get('X-Webhook-Signature')
            if not signature:
                return jsonify({'error': 'Missing signature'}), 400

            try:
                event = construct_event(
                    request.get_data(),
                    signature,
                    secret,
                    tolerance
                )
                return f(event, *args, **kwargs)
            except WebhookSignatureError as e:
                return jsonify({'error': str(e)}), 401

        return wrapper
    return decorator

Documentation Standards

API Documentation Template

# Webhooks

Webhooks allow you to receive real-time notifications when events happen in your account.

## Setting Up Webhooks

1. Create a webhook endpoint in your dashboard or via API
2. Configure which events to receive
3. Implement signature verification
4. Return a 2xx response within 30 seconds

## Event Types

| Event | Description |
|-------|-------------|
| `order.created` | Sent when a new order is placed |
| `order.paid` | Sent when payment is confirmed |
| `order.fulfilled` | Sent when order ships |
| `customer.created` | Sent when a customer registers |

## Payload Format

All events follow this structure:

```json
{
  "id": "evt_1234567890abcdef",
  "type": "order.created",
  "api_version": "2024-01-15",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "object": {
      // Resource data
    }
  },
  "livemode": true
}

Signature Verification

All webhook requests include a signature header for verification:

X-Webhook-Signature: t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Verification Steps

  1. Extract timestamp (t) and signature (v1) from header
  2. Concatenate timestamp + "." + raw request body
  3. Compute HMAC-SHA256 using your endpoint secret
  4. Compare signatures using constant-time comparison

Code Examples

Node.js:

const event = yourapp.webhooks.constructEvent(
  req.body,
  req.headers['x-webhook-signature'],
  process.env.WEBHOOK_SECRET
);

Python:

event = yourapp.webhooks.construct_event(
    request.get_data(),
    request.headers['X-Webhook-Signature'],
    os.environ['WEBHOOK_SECRET']
)

Retry Policy

Failed deliveries are retried with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

Retries stop after:

  • Successful delivery (2xx response)
  • 4xx response (except 429)
  • Maximum attempts reached

Best Practices

  1. Respond quickly - Return 200 within 30 seconds
  2. Process asynchronously - Queue events for processing
  3. Verify signatures - Always validate before processing
  4. Handle duplicates - Events may be delivered more than once
  5. Use HTTPS - Required for production endpoints

Testing

Use our CLI to test webhooks locally:

yourapp webhooks listen --forward-to localhost:3000/webhooks
yourapp webhooks trigger order.created

Or use the webhook simulator in your dashboard.


### OpenAPI Specification

```yaml
# openapi.yaml (webhook section)
paths:
  /webhook-endpoints:
    get:
      summary: List webhook endpoints
      tags: [Webhooks]
      responses:
        '200':
          description: List of endpoints
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'

    post:
      summary: Create webhook endpoint
      tags: [Webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS URL for webhook delivery
                description:
                  type: string
                events:
                  type: array
                  items:
                    type: string
                  description: Event types to subscribe to

components:
  schemas:
    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
          example: we_abc123
        url:
          type: string
          format: uri
        description:
          type: string
        subscribed_events:
          type: array
          items:
            type: string
        enabled:
          type: boolean
        status:
          type: string
          enum: [healthy, degraded, disabled]
        created_at:
          type: string
          format: date-time

    WebhookEvent:
      type: object
      properties:
        id:
          type: string
          example: evt_abc123
        type:
          type: string
          example: order.created
        api_version:
          type: string
          example: '2024-01-15'
        created_at:
          type: string
          format: date-time
        data:
          type: object
          properties:
            object:
              type: object
        livemode:
          type: boolean

Testing Infrastructure

Webhook Simulator

// simulator.ts
router.post('/webhook-endpoints/:id/test', async (req, res) => {
  const customerId = req.auth.customerId;
  const endpointId = req.params.id;
  const { event_type } = req.body;

  const endpoint = await getEndpointIfOwned(endpointId, customerId);
  if (!endpoint) {
    return res.status(404).json({ error: { code: 'not_found' } });
  }

  // Create test event
  const testEvent = createTestEvent(event_type || 'ping');

  // Deliver synchronously for immediate feedback
  const result = await deliverWebhook(endpoint, testEvent);

  res.json({
    data: {
      event: testEvent,
      delivery: {
        success: result.success,
        status_code: result.statusCode,
        response_body: result.responseBody?.slice(0, 500),
        duration_ms: result.duration
      }
    }
  });
});

function createTestEvent(type: string): WebhookEvent {
  const templates: Record<string, () => any> = {
    'ping': () => ({
      message: 'Webhook endpoint verified'
    }),
    'order.created': () => ({
      id: `ord_test_${nanoid(10)}`,
      customer_id: `cus_test_${nanoid(10)}`,
      total: 9999,
      currency: 'usd',
      status: 'pending',
      items: [
        { name: 'Test Product', quantity: 1, price: 9999 }
      ],
      created_at: new Date().toISOString()
    }),
    // Add more templates...
  };

  const dataGenerator = templates[type] || templates['ping'];

  return {
    id: `evt_test_${nanoid(24)}`,
    type,
    api_version: '2024-01-15',
    created_at: new Date().toISOString(),
    data: { object: dataGenerator() },
    livemode: false
  };
}

Summary

Building a webhook provider requires:

  1. Event Model - Consistent envelope with unique IDs, types, and versioning
  2. Delivery Infrastructure - Reliable queue-based delivery with retries
  3. Security - HMAC signatures with timestamps to prevent replay attacks
  4. Management API - Full CRUD for endpoints with delivery history
  5. SDKs - Signature verification libraries in popular languages
  6. Documentation - Comprehensive guides, examples, and testing tools

The key principles are: make integration easy (SDKs, clear docs), make debugging easy (delivery logs, simulator), and make it reliable (retries, health monitoring). A well-built webhook system becomes a critical integration point that your customers depend on.

Let's turn this knowledge into action

Get a free 30-minute consultation with our experts. We'll help you apply these insights to your specific situation.

API Development & Security Testing Workflow: OWASP API Security Top 10 Guide

API Development & Security Testing Workflow: OWASP API Security Top 10 Guide

Build secure APIs with this 7-stage workflow covering design, authentication, development, security testing, integration testing, deployment, and monitoring. Includes OWASP API Top 10 2023 coverage, OAuth 2.0, JWT, rate limiting, and webhook security.

The Complete Developer Debugging & Data Transformation Workflow

The Complete Developer Debugging & Data Transformation Workflow

Reduce debugging time by 50% with this systematic 7-stage workflow. Learn error detection, log analysis, data format validation, API debugging, SQL optimization, regex testing, and documentation strategies with 10 integrated developer tools.

Incident Response & Forensics Investigation Workflow: NIST & SANS Framework Guide

Incident Response & Forensics Investigation Workflow: NIST & SANS Framework Guide

Learn the complete incident response workflow following NIST SP 800-61r3 and SANS 6-step methodology. From preparation to post-incident analysis, this guide covers evidence preservation, forensic collection, threat intelligence, and compliance reporting.

Email Security Hardening & Deliverability: The 13-Week SPF, DKIM, DMARC Implementation Guide

Email Security Hardening & Deliverability: The 13-Week SPF, DKIM, DMARC Implementation Guide

Implement email authentication following Google and Yahoo 2025 requirements. This phased 13-week deployment guide covers SPF optimization, DKIM key rotation, DMARC policy enforcement, deliverability testing, and advanced protections like BIMI and MTA-STS.

Infrastructure-as-Code Security & Change Management: Terraform Best Practices 2025

Infrastructure-as-Code Security & Change Management: Terraform Best Practices 2025

Implement secure IaC workflows with Terraform following 2025 best practices. This comprehensive guide covers pre-commit validation, security scanning with tfsec/Checkov, policy-as-code enforcement, automated testing, drift detection, and cost optimization.

Complete Malware Analysis Workflow

Complete Malware Analysis Workflow

Malware analysis workflow for SOC analysts. Covers triage, static analysis, string extraction, and IOC extraction.