Home/Blog/Webhook Platform Integration Guide: Stripe, GitHub, Slack, Shopify & More

Webhook Platform Integration Guide: Stripe, GitHub, Slack, Shopify & More

Master webhook integrations for popular platforms. Learn platform-specific signature verification, payload handling, event types, and best practices for Stripe, GitHub, Slack, Shopify, Twilio, and more.

By Inventive Software Engineering
Webhook Platform Integration Guide: Stripe, GitHub, Slack, Shopify & More

Building integrations with third-party services means working with webhooks. Whether you're processing Stripe payments, responding to GitHub push events, syncing Shopify orders, or building Slack bots, webhooks are how these platforms communicate changes to your application in real-time.

The challenge? Every platform does webhooks differently. Stripe signs payloads with timestamps to prevent replay attacks. GitHub uses a simpler HMAC scheme. Shopify gives you just 5 seconds to respond. Slack requires you to handle URL verification challenges before receiving any events. Each platform has its own signature algorithm, timeout requirements, retry behavior, and payload format.

This inconsistency creates real problems. Code that works perfectly for Stripe webhooks will fail silently with Shopify's base64-encoded signatures. A handler that processes GitHub events correctly might timeout on Slack's strict 3-second limit. And without proper idempotency handling, you'll process the same payment twice when Stripe retries after a slow response.

This guide provides production-ready implementations for the most popular webhook providers. For each platform, you'll find working code for signature verification, event handling patterns, and the specific gotchas that cause problems in production. The goal is to give you copy-paste-ready code that handles the edge cases you'd otherwise learn about the hard way.

When debugging webhook issues, having the right tools helps. Our HMAC Signature Calculator lets you verify signatures manually, the Base64 Encoder/Decoder handles Shopify's encoding format, and the JSON Formatter makes payload inspection easier. Keep these handy as you work through implementations.

Platform Comparison Overview

Before diving into implementation details, here's a quick reference comparing the key differences between major webhook providers. Understanding these differences upfront will help you design your webhook infrastructure to handle multiple providers without surprises.

┌───────────────────────────────────────────────────────────────────────────────┐
│                    WEBHOOK PLATFORM COMPARISON                                 │
├───────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  Platform   │ Signature          │ Timeout │ Retries │ Event Format          │
│  ─────────  │ ─────────          │ ─────── │ ─────── │ ────────────          │
│  Stripe     │ HMAC-SHA256+time   │ 20s     │ 3 days  │ Versioned API         │
│  GitHub     │ HMAC-SHA256        │ 10s     │ None*   │ X-GitHub-Event header │
│  Shopify    │ HMAC-SHA256        │ 5s      │ 48 hrs  │ X-Shopify-Topic       │
│  Slack      │ HMAC-SHA256+time   │ 3s      │ 3 tries │ Event wrapper         │
│  Twilio     │ HMAC-SHA1/256      │ 15s     │ Variable│ Form or JSON          │
│  PayPal     │ Cert verification  │ 30s     │ 3 days  │ Event type in body    │
│  Square     │ HMAC-SHA256        │ 10s     │ 72 hrs  │ Event wrapper         │
│                                                                               │
│  * GitHub requires manual retry via API                                       │
│                                                                               │
└───────────────────────────────────────────────────────────────────────────────┘

Stripe Webhooks

Stripe is the gold standard for webhook implementations. Their system includes timestamp-based replay attack prevention, automatic retries for up to 3 days, and excellent developer tooling. The Stripe CLI makes local development painless, and their SDK handles signature verification automatically. If you're only integrating with one webhook provider, Stripe's patterns are worth studying as a reference implementation. When debugging signature issues, you can use our HMAC Signature Calculator to manually verify that your computed signature matches Stripe's.

Signature Verification

Stripe uses a timestamp-based signature to prevent replay attacks:

// stripe-webhooks.ts
import Stripe from 'stripe';
import express from 'express';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

const app = express();

// CRITICAL: Use raw body for signature verification
app.post('/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['stripe-signature'] as string;

    let event: Stripe.Event;

    try {
      // Using Stripe SDK (recommended)
      event = stripe.webhooks.constructEvent(
        req.body,
        signature,
        webhookSecret
      );
    } catch (err) {
      console.error('Signature verification failed:', err);
      return res.status(400).send(`Webhook Error: ${err}`);
    }

    // Handle the event
    try {
      await handleStripeEvent(event);
      res.json({ received: true });
    } catch (err) {
      console.error('Event handling failed:', err);
      res.status(500).json({ error: 'Processing failed' });
    }
  }
);

// Manual signature verification (for understanding)
function verifyStripeSignature(
  payload: string,
  signature: string,
  secret: string,
  tolerance: number = 300
): { timestamp: number; verified: boolean } {
  const parts = signature.split(',');
  const timestamp = parseInt(parts.find(p => p.startsWith('t='))?.slice(2) || '0');
  const signatures = parts
    .filter(p => p.startsWith('v1='))
    .map(p => p.slice(3));

  // Check timestamp tolerance (default 5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > tolerance) {
    throw new Error('Timestamp outside tolerance');
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  const verified = signatures.some(sig =>
    crypto.timingSafeEqual(
      Buffer.from(sig),
      Buffer.from(expectedSignature)
    )
  );

  return { timestamp, verified };
}

Common Stripe Event Handlers

// stripe-handlers.ts
import Stripe from 'stripe';

async function handleStripeEvent(event: Stripe.Event): Promise<void> {
  // Check idempotency first
  if (await isEventProcessed(event.id)) {
    console.log(`Duplicate event: ${event.id}`);
    return;
  }

  switch (event.type) {
    // Checkout flow
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object as Stripe.Checkout.Session);
      break;

    // Payments
    case 'payment_intent.succeeded':
      await handlePaymentSucceeded(event.data.object as Stripe.PaymentIntent);
      break;

    case 'payment_intent.payment_failed':
      await handlePaymentFailed(event.data.object as Stripe.PaymentIntent);
      break;

    // Subscriptions
    case 'customer.subscription.created':
    case 'customer.subscription.updated':
      await handleSubscriptionChange(event.data.object as Stripe.Subscription);
      break;

    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object as Stripe.Subscription);
      break;

    // Invoices
    case 'invoice.paid':
      await handleInvoicePaid(event.data.object as Stripe.Invoice);
      break;

    case 'invoice.payment_failed':
      await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice);
      break;

    // Disputes
    case 'charge.dispute.created':
      await handleDisputeCreated(event.data.object as Stripe.Dispute);
      break;

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  await markEventProcessed(event.id);
}

async function handleCheckoutComplete(session: Stripe.Checkout.Session): Promise<void> {
  const orderId = session.metadata?.order_id;

  if (!orderId) {
    console.error('No order_id in session metadata');
    return;
  }

  // Fulfill the order
  await db.query(`
    UPDATE orders
    SET status = 'paid',
        stripe_session_id = $1,
        paid_at = NOW()
    WHERE id = $2
  `, [session.id, orderId]);

  // Send confirmation email
  await sendOrderConfirmation(orderId);
}

async function handleSubscriptionChange(subscription: Stripe.Subscription): Promise<void> {
  const customerId = subscription.customer as string;

  await db.query(`
    INSERT INTO subscriptions (
      stripe_subscription_id,
      stripe_customer_id,
      status,
      current_period_start,
      current_period_end,
      cancel_at_period_end
    ) VALUES ($1, $2, $3, $4, $5, $6)
    ON CONFLICT (stripe_subscription_id)
    DO UPDATE SET
      status = EXCLUDED.status,
      current_period_start = EXCLUDED.current_period_start,
      current_period_end = EXCLUDED.current_period_end,
      cancel_at_period_end = EXCLUDED.cancel_at_period_end,
      updated_at = NOW()
  `, [
    subscription.id,
    customerId,
    subscription.status,
    new Date(subscription.current_period_start * 1000),
    new Date(subscription.current_period_end * 1000),
    subscription.cancel_at_period_end
  ]);
}

GitHub Webhooks

GitHub webhooks power CI/CD pipelines, deployment automation, and repository monitoring across millions of projects. Their implementation uses straightforward HMAC-SHA256 signatures without timestamps, making verification simpler than Stripe but without built-in replay protection. One important limitation: GitHub doesn't automatically retry failed deliveries, so you'll need to handle retries manually through their API or build your own retry logic. The HMAC Signature Calculator is useful for verifying GitHub signatures during debugging—just remember to use SHA256 and hex encoding.

Signature Verification

GitHub uses simpler HMAC-SHA256 without timestamp:

// github-webhooks.ts
import crypto from 'crypto';
import express from 'express';

const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET!;

app.post('/webhooks/github',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['x-hub-signature-256'] as string;
    const event = req.headers['x-github-event'] as string;
    const deliveryId = req.headers['x-github-delivery'] as string;

    // Verify signature
    if (!verifyGitHubSignature(req.body, signature, webhookSecret)) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(req.body.toString());

    // Handle based on event type
    try {
      await handleGitHubEvent(event, payload, deliveryId);
      res.status(200).send('OK');
    } catch (err) {
      console.error('GitHub webhook error:', err);
      res.status(500).send('Processing failed');
    }
  }
);

function verifyGitHubSignature(
  payload: Buffer,
  signature: string,
  secret: string
): boolean {
  if (!signature || !signature.startsWith('sha256=')) {
    return false;
  }

  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Common GitHub Event Handlers

// github-handlers.ts
interface GitHubWebhookPayload {
  action?: string;
  repository: {
    full_name: string;
    default_branch: string;
  };
  sender: {
    login: string;
  };
  [key: string]: any;
}

async function handleGitHubEvent(
  event: string,
  payload: GitHubWebhookPayload,
  deliveryId: string
): Promise<void> {
  // Check idempotency
  if (await isDeliveryProcessed(deliveryId)) {
    return;
  }

  console.log(`Processing ${event} from ${payload.repository.full_name}`);

  switch (event) {
    case 'push':
      await handlePush(payload);
      break;

    case 'pull_request':
      await handlePullRequest(payload);
      break;

    case 'issues':
      await handleIssue(payload);
      break;

    case 'issue_comment':
      await handleIssueComment(payload);
      break;

    case 'release':
      await handleRelease(payload);
      break;

    case 'workflow_run':
      await handleWorkflowRun(payload);
      break;

    case 'check_suite':
      await handleCheckSuite(payload);
      break;

    default:
      console.log(`Unhandled GitHub event: ${event}`);
  }

  await markDeliveryProcessed(deliveryId);
}

async function handlePush(payload: any): Promise<void> {
  const { ref, commits, repository, pusher } = payload;

  // Only process main branch pushes
  if (ref !== `refs/heads/${repository.default_branch}`) {
    return;
  }

  // Trigger deployment
  await triggerDeployment({
    repo: repository.full_name,
    branch: repository.default_branch,
    commits: commits.map((c: any) => ({
      sha: c.id,
      message: c.message,
      author: c.author.name
    })),
    pusher: pusher.name
  });
}

async function handlePullRequest(payload: any): Promise<void> {
  const { action, pull_request, repository } = payload;

  switch (action) {
    case 'opened':
    case 'synchronize':
      // Run CI checks
      await triggerCI({
        repo: repository.full_name,
        prNumber: pull_request.number,
        headSha: pull_request.head.sha,
        baseBranch: pull_request.base.ref
      });
      break;

    case 'closed':
      if (pull_request.merged) {
        // Clean up PR environment
        await cleanupPREnvironment(pull_request.number);
      }
      break;
  }
}

async function handleRelease(payload: any): Promise<void> {
  const { action, release, repository } = payload;

  if (action === 'published') {
    // Notify team of new release
    await notifySlack({
      channel: '#releases',
      text: `🚀 ${repository.full_name} ${release.tag_name} released by ${release.author.login}`,
      attachments: [{
        text: release.body,
        color: 'good'
      }]
    });

    // Update changelog
    await updateChangelog(repository.full_name, release);
  }
}

Shopify Webhooks

Shopify webhooks are critical for e-commerce integrations, handling everything from order processing to inventory synchronization. The key challenge with Shopify is their aggressive 5-second timeout—if your endpoint doesn't respond in time, Shopify marks the delivery as failed. This means you must acknowledge webhooks immediately and process the actual business logic asynchronously. Shopify also uses base64-encoded HMAC signatures instead of hex, which catches many developers off guard—use our Base64 Encoder/Decoder to convert between formats when debugging.

Signature Verification

// shopify-webhooks.ts
import crypto from 'crypto';
import express from 'express';

const shopifySecret = process.env.SHOPIFY_WEBHOOK_SECRET!;

app.post('/webhooks/shopify',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const hmac = req.headers['x-shopify-hmac-sha256'] as string;
    const topic = req.headers['x-shopify-topic'] as string;
    const shopDomain = req.headers['x-shopify-shop-domain'] as string;

    // Verify signature
    if (!verifyShopifySignature(req.body, hmac, shopifySecret)) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(req.body.toString());

    // IMPORTANT: Respond quickly (5 second timeout)
    res.status(200).send('OK');

    // Process asynchronously
    try {
      await handleShopifyEvent(topic, payload, shopDomain);
    } catch (err) {
      console.error('Shopify webhook error:', err);
    }
  }
);

function verifyShopifySignature(
  body: Buffer,
  hmac: string,
  secret: string
): boolean {
  const calculatedHmac = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('base64');

  return crypto.timingSafeEqual(
    Buffer.from(hmac),
    Buffer.from(calculatedHmac)
  );
}

Common Shopify Event Handlers

// shopify-handlers.ts
async function handleShopifyEvent(
  topic: string,
  payload: any,
  shopDomain: string
): Promise<void> {
  console.log(`Processing ${topic} from ${shopDomain}`);

  switch (topic) {
    // Orders
    case 'orders/create':
      await handleOrderCreated(payload, shopDomain);
      break;

    case 'orders/paid':
      await handleOrderPaid(payload, shopDomain);
      break;

    case 'orders/fulfilled':
      await handleOrderFulfilled(payload, shopDomain);
      break;

    case 'orders/cancelled':
      await handleOrderCancelled(payload, shopDomain);
      break;

    // Customers
    case 'customers/create':
      await handleCustomerCreated(payload, shopDomain);
      break;

    case 'customers/update':
      await handleCustomerUpdated(payload, shopDomain);
      break;

    // Products
    case 'products/create':
    case 'products/update':
      await syncProduct(payload, shopDomain);
      break;

    case 'products/delete':
      await deleteProduct(payload.id, shopDomain);
      break;

    // Inventory
    case 'inventory_levels/update':
      await handleInventoryUpdate(payload, shopDomain);
      break;

    // App lifecycle
    case 'app/uninstalled':
      await handleAppUninstalled(shopDomain);
      break;

    default:
      console.log(`Unhandled Shopify topic: ${topic}`);
  }
}

async function handleOrderCreated(order: any, shopDomain: string): Promise<void> {
  // Sync order to your system
  await db.query(`
    INSERT INTO shopify_orders (
      shop_domain,
      order_id,
      order_number,
      customer_email,
      total_price,
      currency,
      line_items,
      shipping_address,
      created_at
    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
    ON CONFLICT (shop_domain, order_id) DO UPDATE SET
      total_price = EXCLUDED.total_price,
      line_items = EXCLUDED.line_items,
      updated_at = NOW()
  `, [
    shopDomain,
    order.id,
    order.order_number,
    order.email,
    order.total_price,
    order.currency,
    JSON.stringify(order.line_items),
    JSON.stringify(order.shipping_address),
    order.created_at
  ]);

  // Notify fulfillment system
  if (order.fulfillment_status === null) {
    await queueForFulfillment({
      orderId: order.id,
      shopDomain,
      items: order.line_items.map((item: any) => ({
        sku: item.sku,
        quantity: item.quantity
      }))
    });
  }
}

async function handleInventoryUpdate(payload: any, shopDomain: string): Promise<void> {
  const { inventory_item_id, location_id, available } = payload;

  // Update inventory in your system
  await db.query(`
    UPDATE inventory
    SET quantity = $1, updated_at = NOW()
    WHERE shop_domain = $2
      AND inventory_item_id = $3
      AND location_id = $4
  `, [available, shopDomain, inventory_item_id, location_id]);

  // Check for low stock alerts
  if (available <= 10) {
    await sendLowStockAlert({
      shopDomain,
      inventoryItemId: inventory_item_id,
      available
    });
  }
}

Slack Webhooks

Slack's webhook system is more complex than other providers because it handles multiple interaction types: event subscriptions, slash commands, interactive components (buttons and menus), and shortcuts. Each type has slightly different payload formats, but they all share the same signature verification scheme. The biggest gotcha is Slack's strict 3-second timeout—the tightest of any major provider. You must also handle Slack's URL verification challenge when first setting up your endpoint.

Event Subscriptions vs Slash Commands

// slack-webhooks.ts
import crypto from 'crypto';
import express from 'express';

const slackSigningSecret = process.env.SLACK_SIGNING_SECRET!;

// Slack requires responding within 3 seconds
app.post('/webhooks/slack/events',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    // Verify signature
    if (!verifySlackSignature(req)) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(req.body.toString());

    // Handle URL verification challenge
    if (payload.type === 'url_verification') {
      return res.json({ challenge: payload.challenge });
    }

    // Acknowledge immediately
    res.status(200).send();

    // Process asynchronously
    if (payload.type === 'event_callback') {
      await handleSlackEvent(payload.event, payload.team_id);
    }
  }
);

function verifySlackSignature(req: express.Request): boolean {
  const timestamp = req.headers['x-slack-request-timestamp'] as string;
  const signature = req.headers['x-slack-signature'] as string;

  // Prevent replay attacks (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }

  const sigBasestring = `v0:${timestamp}:${req.body.toString()}`;
  const expectedSignature = 'v0=' + crypto
    .createHmac('sha256', slackSigningSecret)
    .update(sigBasestring)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Slack Event Handlers

// slack-handlers.ts
interface SlackEvent {
  type: string;
  user?: string;
  channel?: string;
  text?: string;
  ts?: string;
  [key: string]: any;
}

async function handleSlackEvent(event: SlackEvent, teamId: string): Promise<void> {
  switch (event.type) {
    case 'message':
      // Ignore bot messages to prevent loops
      if (event.subtype === 'bot_message' || event.bot_id) {
        return;
      }
      await handleMessage(event, teamId);
      break;

    case 'app_mention':
      await handleMention(event, teamId);
      break;

    case 'member_joined_channel':
      await handleMemberJoined(event, teamId);
      break;

    case 'reaction_added':
      await handleReactionAdded(event, teamId);
      break;

    case 'app_home_opened':
      await updateAppHome(event.user!, teamId);
      break;

    default:
      console.log(`Unhandled Slack event: ${event.type}`);
  }
}

async function handleMention(event: SlackEvent, teamId: string): Promise<void> {
  const { text, channel, user, ts } = event;

  // Parse command from mention
  const command = text?.replace(/<@[A-Z0-9]+>/g, '').trim().toLowerCase();

  // Respond in thread
  await slackClient.chat.postMessage({
    channel: channel!,
    thread_ts: ts,
    text: await processCommand(command || '', user!, teamId)
  });
}

// Interactive Components (buttons, menus)
app.post('/webhooks/slack/interactions',
  express.urlencoded({ extended: true }),
  async (req, res) => {
    const payload = JSON.parse(req.body.payload);

    // Verify signature (same as events)
    if (!verifySlackSignature(req)) {
      return res.status(401).send('Invalid signature');
    }

    // Handle interaction
    switch (payload.type) {
      case 'block_actions':
        await handleBlockAction(payload);
        break;

      case 'view_submission':
        await handleViewSubmission(payload);
        break;

      case 'shortcut':
        await handleShortcut(payload);
        break;
    }

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

async function handleBlockAction(payload: any): Promise<void> {
  const action = payload.actions[0];
  const userId = payload.user.id;

  switch (action.action_id) {
    case 'approve_request':
      await approveRequest(action.value, userId);
      // Update the original message
      await slackClient.chat.update({
        channel: payload.channel.id,
        ts: payload.message.ts,
        text: `✅ Approved by <@${userId}>`,
        blocks: [] // Remove buttons
      });
      break;

    case 'deny_request':
      await denyRequest(action.value, userId);
      break;
  }
}

Twilio Webhooks

Twilio webhooks handle SMS, voice calls, and other communication channels. Unlike other providers that expect JSON responses, Twilio uses TwiML (Twilio Markup Language)—an XML-based format that controls call flows and message responses. Twilio's signature verification is URL-based, meaning the full webhook URL (including query parameters) is part of the signature calculation. This can cause verification failures if your URL differs between environments or if a proxy modifies the URL.

Signature Verification

// twilio-webhooks.ts
import twilio from 'twilio';
import express from 'express';

const authToken = process.env.TWILIO_AUTH_TOKEN!;

app.post('/webhooks/twilio/sms',
  express.urlencoded({ extended: true }),
  async (req, res) => {
    const signature = req.headers['x-twilio-signature'] as string;
    const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;

    // Verify using Twilio library
    const valid = twilio.validateRequest(
      authToken,
      signature,
      url,
      req.body
    );

    if (!valid) {
      return res.status(401).send('Invalid signature');
    }

    await handleTwilioSMS(req.body);

    // Respond with TwiML
    const twiml = new twilio.twiml.MessagingResponse();
    twiml.message('Thanks for your message!');

    res.type('text/xml');
    res.send(twiml.toString());
  }
);

// Twilio Voice webhooks
app.post('/webhooks/twilio/voice',
  express.urlencoded({ extended: true }),
  async (req, res) => {
    const signature = req.headers['x-twilio-signature'] as string;
    const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;

    if (!twilio.validateRequest(authToken, signature, url, req.body)) {
      return res.status(401).send('Invalid signature');
    }

    const twiml = new twilio.twiml.VoiceResponse();

    // Handle incoming call
    if (req.body.CallStatus === 'ringing') {
      twiml.say('Hello, you have reached our support line.');
      twiml.gather({
        numDigits: 1,
        action: '/webhooks/twilio/voice/menu'
      }).say('Press 1 for sales, press 2 for support.');
    }

    res.type('text/xml');
    res.send(twiml.toString());
  }
);

SMS and Voice Handlers

// twilio-handlers.ts
interface TwilioSMSPayload {
  MessageSid: string;
  From: string;
  To: string;
  Body: string;
  NumMedia?: string;
  MediaUrl0?: string;
}

async function handleTwilioSMS(payload: TwilioSMSPayload): Promise<void> {
  const { MessageSid, From, To, Body } = payload;

  // Log incoming message
  await db.query(`
    INSERT INTO sms_messages (
      message_sid, from_number, to_number, body, direction, received_at
    ) VALUES ($1, $2, $3, $4, 'inbound', NOW())
  `, [MessageSid, From, To, Body]);

  // Parse commands
  const command = Body.trim().toUpperCase();

  switch (command) {
    case 'STOP':
      await handleOptOut(From);
      break;

    case 'START':
    case 'UNSTOP':
      await handleOptIn(From);
      break;

    case 'HELP':
      await sendHelpMessage(From);
      break;

    default:
      // Handle as regular message
      await processIncomingMessage(From, Body);
  }
}

// Call status webhooks
app.post('/webhooks/twilio/status',
  express.urlencoded({ extended: true }),
  async (req, res) => {
    const { CallSid, CallStatus, CallDuration, From, To } = req.body;

    await db.query(`
      UPDATE calls
      SET status = $1, duration = $2, updated_at = NOW()
      WHERE call_sid = $3
    `, [CallStatus, CallDuration, CallSid]);

    if (CallStatus === 'completed') {
      // Process call recording if enabled
      if (req.body.RecordingUrl) {
        await processRecording(CallSid, req.body.RecordingUrl);
      }
    }

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

Multi-Provider Router

When your application integrates with multiple webhook providers, you'll want a unified architecture that handles routing, verification, and processing consistently. A well-designed multi-provider router abstracts away provider-specific details while ensuring each webhook is verified and processed correctly. This pattern also makes it easier to add new providers and maintain consistent logging, error handling, and idempotency across all integrations. When inspecting webhook payloads from different providers, the JSON Formatter helps you quickly compare payload structures and identify differences.

Unified Webhook Handler

// webhook-router.ts
import express from 'express';
import { verifyStripeSignature } from './providers/stripe';
import { verifyGitHubSignature } from './providers/github';
import { verifyShopifySignature } from './providers/shopify';
import { verifySlackSignature } from './providers/slack';

const app = express();

// Provider-specific middleware
const providers = {
  stripe: {
    verify: verifyStripeSignature,
    getEventType: (req: express.Request, payload: any) => payload.type,
    getEventId: (req: express.Request, payload: any) => payload.id
  },
  github: {
    verify: verifyGitHubSignature,
    getEventType: (req: express.Request) => req.headers['x-github-event'] as string,
    getEventId: (req: express.Request) => req.headers['x-github-delivery'] as string
  },
  shopify: {
    verify: verifyShopifySignature,
    getEventType: (req: express.Request) => req.headers['x-shopify-topic'] as string,
    getEventId: (req: express.Request, payload: any) => `${payload.id}-${req.headers['x-shopify-topic']}`
  },
  slack: {
    verify: verifySlackSignature,
    getEventType: (req: express.Request, payload: any) =>
      payload.type === 'event_callback' ? payload.event.type : payload.type,
    getEventId: (req: express.Request, payload: any) =>
      payload.event_id || payload.event?.ts || crypto.randomUUID()
  }
};

// Generic webhook endpoint
app.post('/webhooks/:provider',
  express.raw({ type: '*/*' }),
  async (req, res) => {
    const provider = req.params.provider as keyof typeof providers;

    if (!providers[provider]) {
      return res.status(404).json({ error: 'Unknown provider' });
    }

    const config = providers[provider];

    // Verify signature
    if (!config.verify(req)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const payload = JSON.parse(req.body.toString());
    const eventType = config.getEventType(req, payload);
    const eventId = config.getEventId(req, payload);

    // Check idempotency
    if (await isProcessed(provider, eventId)) {
      return res.status(200).json({ status: 'duplicate' });
    }

    // Queue for processing
    await webhookQueue.add(`${provider}-webhook`, {
      provider,
      eventType,
      eventId,
      payload,
      headers: req.headers,
      receivedAt: Date.now()
    });

    res.status(200).json({ received: true });
  }
);

Provider Configuration Management

// provider-config.ts
interface ProviderConfig {
  name: string;
  webhookSecret: string;
  enabled: boolean;
  endpoints: string[];
  events: string[];
}

class WebhookProviderManager {
  private configs = new Map<string, ProviderConfig>();

  async loadConfigs(): Promise<void> {
    const configs = await db.query(`
      SELECT * FROM webhook_providers WHERE enabled = true
    `);

    for (const config of configs.rows) {
      this.configs.set(config.name, {
        name: config.name,
        webhookSecret: config.webhook_secret,
        enabled: config.enabled,
        endpoints: config.endpoints,
        events: config.events
      });
    }
  }

  getConfig(provider: string): ProviderConfig | undefined {
    return this.configs.get(provider);
  }

  async updateSecret(provider: string, secret: string): Promise<void> {
    await db.query(`
      UPDATE webhook_providers
      SET webhook_secret = $1, updated_at = NOW()
      WHERE name = $2
    `, [secret, provider]);

    const config = this.configs.get(provider);
    if (config) {
      config.webhookSecret = secret;
    }
  }

  async enableEvent(provider: string, event: string): Promise<void> {
    await db.query(`
      UPDATE webhook_providers
      SET events = array_append(events, $1)
      WHERE name = $2 AND NOT ($1 = ANY(events))
    `, [event, provider]);

    await this.loadConfigs();
  }
}

export const providerManager = new WebhookProviderManager();

Testing Provider Integrations

Testing webhooks locally is notoriously difficult because providers need to reach your development machine. Each provider offers different approaches: CLI tools that forward events, webhook forwarding services, or APIs to trigger test events. Using these tools during development prevents the frustrating cycle of deploying to test webhooks and discovering signature verification failures in production. When writing integration tests, you'll need unique event IDs for idempotency testing—our UUID Generator can create these quickly.

Stripe Testing

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhooks to local
stripe listen --forward-to localhost:3000/webhooks/stripe

# Trigger events
stripe trigger payment_intent.succeeded
stripe trigger checkout.session.completed
stripe trigger customer.subscription.created

GitHub Testing

# Using smee.io for webhook forwarding
npx smee-client --url https://smee.io/your-channel --target http://localhost:3000/webhooks/github

# Configure webhook in GitHub repo settings to use smee.io URL

Shopify Testing

// Shopify provides test webhooks via API
const response = await fetch(
  `https://${shop}/admin/api/2024-01/webhooks.json`,
  {
    method: 'POST',
    headers: {
      'X-Shopify-Access-Token': accessToken,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      webhook: {
        topic: 'orders/create',
        address: 'https://your-tunnel.ngrok.io/webhooks/shopify',
        format: 'json'
      }
    })
  }
);

Summary

Each webhook provider has unique characteristics:

  • Stripe: Most developer-friendly, excellent CLI tools, timestamp-based signatures
  • GitHub: Simple signature scheme, comprehensive event types, manual retry required
  • Shopify: Very short timeout (5s), base64 HMAC signatures, topic-based routing
  • Slack: Complex interaction model, strict 3-second timeout, URL verification challenge
  • Twilio: XML (TwiML) responses, URL-based signature validation

The key to successful multi-provider webhook handling is:

  1. Use raw body for signature verification before any parsing
  2. Respond quickly within provider timeouts
  3. Process asynchronously for anything complex
  4. Implement idempotency using provider-specific event IDs
  5. Use official SDKs when available for signature verification

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.

Webhook Development Complete Guide: Architecture, Security, and Best Practices

Webhook Development Complete Guide: Architecture, Security, and Best Practices

Master webhook development from fundamentals to production. Learn architecture patterns, signature verification, retry logic, error handling, and platform integrations for reliable event-driven systems.

Webhook Signature Verification: Complete Security Guide

Webhook Signature Verification: Complete Security Guide

Master webhook signature verification across HMAC-SHA256, HMAC-SHA1, RSA-SHA256, and ECDSA algorithms. Learn implementation patterns, security best practices, and avoid common mistakes with production-ready code examples.

Webhook Security: Complete Guide to Securing Webhook Endpoints

Webhook Security: Complete Guide to Securing Webhook Endpoints

Master webhook security with this comprehensive guide covering authentication methods, common vulnerabilities, implementation best practices, and real-world attack prevention strategies for securing webhook endpoints.

Webhook Testing & Debugging: Complete Guide to Local Development and Troubleshooting

Webhook Testing & Debugging: Complete Guide to Local Development and Troubleshooting

Master webhook testing and debugging with ngrok, Cloudflare Tunnel, RequestBin, and custom test harnesses. Learn systematic approaches to troubleshoot webhook failures in development and production.

Webhook Error Handling & Recovery: Dead Letter Queues, Alerting, and Failure Recovery

Webhook Error Handling & Recovery: Dead Letter Queues, Alerting, and Failure Recovery

Build resilient webhook systems with comprehensive error handling. Learn dead letter queues, circuit breakers, automatic recovery, alerting strategies, and techniques for handling failures gracefully.

Testing Webhooks Locally with ngrok: Complete Guide

Testing Webhooks Locally with ngrok: Complete Guide

Master local webhook testing with ngrok. Learn how to expose your development server to the internet, inspect webhook payloads, and debug integrations before deploying to production.