Back to Blog
ยทHook Mesh Team

Webhook Integration Guides: Connect with Any Platform

Comprehensive guides for integrating webhooks from Stripe, GitHub, Shopify, Twilio, and 10+ other platforms. Learn best practices for reliable webhook handling across your entire stack.

Webhook Integration Guides: Connect with Any Platform

Quick Reference

Multi-platform webhook integration architecture showing a central webhook gateway connected to payment, developer, communication, CRM, and authentication platforms with bidirectional event flows to destination servers

A quick comparison of how major providers handle webhook delivery:

PlatformSignatureTimeoutRetriesTest Tool
StripeHMAC-SHA25630s3 days, exponential backoffStripe CLI
PayPalCertificate chain30s3 daysSandbox environment
ShopifyHMAC-SHA2565s48 hoursPartner dashboard
GitHubHMAC-SHA25610sLimited (manual redeliver)webhook.site
TwilioHMAC-SHA115sNone by defaultRequest inspector
SendGridECDSA30s24 hoursEvent webhook tester
SlackHMAC-SHA2563s3 retriesSlack CLI
HubSpotNone (IP allowlist)5s10 attemptsWorkflow tester
ClerkHMAC-SHA256 (Svix)15sMultiple daysSvix dashboard

Table of Contents


Payments and E-commerce

Platform Guides

Stripe Webhooks

Verify Stripe signatures, handle critical payment and subscription events, build idempotent handlers.

AttributeValue
SignatureHMAC-SHA256
HeaderStripe-Signature
Timeout30 seconds
Retries3 days, exponential backoff
Test ToolStripe CLI

Most Important Events

  • Payments: payment_intent.succeeded, payment_intent.payment_failed, charge.failed
  • Subscriptions: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.paid, invoice.payment_failed
  • Checkout: checkout.session.completed, checkout.session.expired
  • Disputes: charge.dispute.created, charge.dispute.closed

Verifying Stripe Signatures in Node.js

Stripe uses HMAC-SHA256 to sign webhook payloads. Always verify signatures to prevent processing forged webhooks:

import crypto from 'crypto';
import express from 'express';

const app = express();
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;

// Important: Use raw body, not parsed JSON
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const body = req.body;

  try {
    // Stripe sends: timestamp.payload.signature
    // We reconstruct and verify
    const [timestamp, hash] = sig.split(',').map(part => {
      const [key, value] = part.split('=');
      return key === 't' ? value : key === 'v1' ? value : null;
    });

    // Use Stripe's official library for production
    const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);

    // Handle event
    switch (event.type) {
      case 'payment_intent.succeeded':
        handlePaymentSuccess(event.data.object);
        break;
      case 'customer.subscription.created':
        handleSubscriptionCreated(event.data.object);
        break;
      // Handle other events...
    }

    res.json({received: true});
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message);
    res.status(400).send(`Webhook Error: ${err.message}`);
  }
});

For production applications, use the official Stripe Node.js library which handles signature verification internally.

PayPal Webhooks

Certificate chain verification, handle both classic and modern webhook formats.

AttributeValue
SignatureCertificate chain verification
HeaderPaypal-Transmission-Sig
Timeout30 seconds
Retries3 days
Test ToolSandbox environment

Most Important Events

  • Transactions: PAYMENT.SALE.COMPLETED, PAYMENT.SALE.DENIED, PAYMENT.SALE.REFUNDED
  • Subscriptions: BILLING.SUBSCRIPTION.CREATED, BILLING.SUBSCRIPTION.UPDATED, BILLING.SUBSCRIPTION.CANCELLED
  • Disputes: CUSTOMER.DISPUTE.CREATED, CUSTOMER.DISPUTE.RESOLVED
  • Account: ACCOUNT.UPDATED, ACCOUNT.MERCHANT.ONBOARDED

Shopify Webhooks

HMAC-SHA256 verification, order and customer events, webhook quotas, retry strategies.

AttributeValue
SignatureHMAC-SHA256
HeaderX-Shopify-Hmac-Sha256
Timeout5 seconds
Retries48 hours, 19 attempts
Test ToolPartner dashboard

Most Important Events

  • Orders: orders/create, orders/paid, orders/cancelled, orders/fulfilled
  • Customers: customers/create, customers/update, customers/delete
  • Products: products/create, products/update, products/delete, inventory/update
  • Shop: app/installed, app/uninstalled, shop/update

Developer Tools

Platform Guides

GitHub Webhooks

HMAC-SHA256 signature verification, push/PR events, GitHub Actions integration, organization webhooks.

AttributeValue
SignatureHMAC-SHA256
HeaderX-Hub-Signature-256
Timeout10 seconds
RetriesLimited (manual redeliver)
Test Toolwebhook.site, Recent Deliveries

Most Important Events

  • Code & Commits: push, create, delete, tag_creation
  • Pull Requests: pull_request, pull_request_review, pull_request_review_comment
  • CI/CD: workflow_run, check_run, check_suite
  • Security: dependabot_alert, secret_scanning_alert, repository_vulnerability_alert
  • Issues: issues, issue_comment, discussions, discussion_comment

Verifying GitHub Signatures in Node.js

GitHub signs webhooks with HMAC-SHA256, encoding the signature in the X-Hub-Signature-256 header. Verify it before processing:

import crypto from 'crypto';
import express from 'express';

const app = express();
const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET;

app.post('/webhooks/github', express.json(), (req, res) => {
  const signature = req.headers['x-hub-signature-256'];
  const payload = JSON.stringify(req.body);

  try {
    // GitHub format: sha256=hex_encoded_hash
    const expectedSignature = 'sha256=' +
      crypto
        .createHmac('sha256', webhookSecret)
        .update(payload)
        .digest('hex');

    // Use timing-safe comparison to prevent timing attacks
    const isValid = crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );

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

    const event = req.body;

    // Handle event
    switch (event.action || event.type) {
      case 'opened': // Pull request or issue opened
        handleOpened(event);
        break;
      case 'push':
        handlePush(event);
        break;
      case 'completed': // Workflow run completed
        if (event.workflow_run) {
          handleWorkflowRun(event);
        }
        break;
      // Handle other events...
    }

    res.json({message: 'OK'});
  } catch (err) {
    console.error('Webhook verification failed:', err.message);
    res.status(400).send('Webhook error: ' + err.message);
  }
});

GitHub includes the webhook ID in X-GitHub-Delivery and the event type in X-GitHub-Event headers for convenient routing.

Supabase Webhooks

Database triggers, realtime subscriptions, Edge Functions, row-level security.


Communication and Email

Platform Guides

Twilio Webhooks

HMAC-SHA1 signature validation, TwiML responses, status callbacks, phone number configuration.

AttributeValue
SignatureHMAC-SHA1
HeaderX-Twilio-Signature
Timeout15 seconds
RetriesNone by default
Test ToolRequest inspector, ngrok

Most Important Events

  • Messages: inbound, message, sms
  • Calls: call, recording, application
  • Status: delivery, connect, hangup
  • Errors: error, exception

SendGrid Webhooks

ECDSA signature verification, delivery confirmation, bounce notifications, engagement tracking.

AttributeValue
SignatureECDSA (Elliptic Curve)
HeaderX-Twilio-Email-Event-Webhook-Signature
Timeout30 seconds
Retries24 hours
Test ToolEvent webhook tester

Most Important Events

  • Delivery: bounce, delivered, dropped, deferred
  • Engagement: open, click, unsubscribe, spam_report
  • Processing: processed, send, invalid_email
  • Suppression: group_unsubscribe, group_resubscribe

Slack Webhooks

HMAC-SHA256 verification, incoming webhooks, Events API, slash commands, interactive messages.

AttributeValue
SignatureHMAC-SHA256
HeaderX-Slack-Signature
Timeout3 seconds
Retries3 retries over 1 hour
Test ToolSlack CLI, Request URL verify

Most Important Events

  • Messages: message, app_mention, message_metadata_posted
  • Channels: channel_created, channel_renamed, channel_deleted
  • Reactions: emoji_changed, reaction_added, reaction_removed
  • Users: user_change, team_join, user_status_changed
  • Interactions: event_callback, url_verification, slash_commands

CRM and Marketing

Platform Guides

HubSpot Webhooks

IP allowlist verification, workflow-based webhooks, contact/deal/company sync, subscription events.

AttributeValue
SignatureNone (use IP allowlist)
HeaderN/A
Timeout5 seconds
Retries10 attempts over 24 hours
Test ToolWorkflow tester

Most Important Events

  • Contacts: contact.creation, contact.change, contact.deletion
  • Deals: deal.creation, deal.change, deal.deletion
  • Companies: company.creation, company.change, company.deletion
  • Workflows: workflow.execution, workflow.event

Authentication

Platform Guides

Clerk Webhooks

Svix HMAC-SHA256 signatures, user lifecycle events, organization webhooks, session events.

AttributeValue
SignatureHMAC-SHA256 (Svix)
Headersvix-signature
Timeout15 seconds
RetriesMultiple days, exponential backoff
Test ToolSvix dashboard

Most Important Events

  • Users: user.created, user.updated, user.deleted
  • Sessions: session.created, session.ended, session.revoked
  • Organizations: organization.created, organization.updated, organization.deleted
  • Memberships: organizationMembership.created, organizationMembership.updated, organizationMembership.deleted
  • Security: email_address.updated, phone_number.updated, password.updated

Testing Webhooks Locally

One of the biggest challenges in webhook development is testing locally. Your development machine isn't accessible from the internet, so external services can't reach your localhost webhook handler. Here are proven solutions:

Stripe CLI

For Stripe specifically, use the Stripe CLI to forward webhooks to your local environment:

# Install Stripe CLI (macOS)
brew install stripe/stripe-cli/stripe

# Authenticate
stripe login

# Forward webhooks to your local server
stripe listen --forward-to localhost:8000/webhooks/stripe

# View all forwarded events
stripe events resend evt_1234567890

ngrok: Free Tunneling Service

ngrok creates a secure tunnel from the internet to your local machine. This works for any webhook provider:

# Install ngrok
brew install ngrok/ngrok/ngrok

# Create tunnel to your local server
ngrok http 8000

# Example output:
# Forwarding                    https://abc123.ngrok.io -> http://localhost:8000

Then configure your webhook URL to https://abc123.ngrok.io/webhooks/stripe (for example). ngrok includes request inspection tools accessible at http://localhost:4040.

Benefits: Works with all providers, shows request/response details, supports authentication.

Drawbacks: Free tier has limited concurrent connections and session time limits.

webhook.site: Inspect Without Code

For quickly inspecting webhook payloads without writing any code, use webhook.site:

  1. Visit webhook.site
  2. Copy your unique URL (e.g., https://webhook.site/abc123-def456)
  3. Configure this URL as your webhook endpoint in the provider's settings
  4. All incoming requests appear in real-time with full payload details

This is perfect for understanding payload structure before building handlers.

Localtunnel: Another Free Alternative

Localtunnel is another tunneling option:

# Install globally
npm install -g localtunnel

# Create tunnel
lt --port 8000 --subdomain my-api

# Tunnel available at: https://my-api.loca.lt

Best Practice: Environment-Based Configuration

In production, separate your webhook handling by environment:

// config/webhooks.js
export const getWebhookConfig = () => {
  if (process.env.NODE_ENV === 'development') {
    // Use ngrok URL from .env.local
    return {
      webhookUrl: process.env.NGROK_URL,
      // Disable signature verification in dev (or use provider's test mode)
    };
  }

  return {
    webhookUrl: process.env.PRODUCTION_WEBHOOK_URL,
    signatureVerification: true,
  };
};

Always test signature verification in at least one environment before deploying to production. Most providers offer test/sandbox modes where you can trigger webhooks without making real transactions.


Troubleshooting Common Webhook Issues

Webhook troubleshooting flowchart showing diagnostic paths for signature failures, timeout errors, duplicate events, and missing events, each leading to resolution steps

Even with careful implementation, webhook integrations can fail silently. Here's how to diagnose and fix the most common problems:

Signature Verification Failures

Problem: Webhooks are rejected with "invalid signature" errors, even though the setup looks correct.

Common Causes:

  1. Timestamp drift: Some providers include a timestamp in the signature calculation. If your server's clock is significantly out of sync, verification fails.

    • Fix: Run ntpdate -s time.nist.gov (macOS) or sync via System Preferences
    • Better: Use a monotonic clock for verification if possible
  2. Raw body vs parsed body: Signature verification must use the exact raw bytes received, not a parsed/re-encoded JSON object.

    • Fix: Use express.raw({type: 'application/json'}) instead of express.json() to preserve the exact request body
    • Wrong: JSON.stringify(JSON.parse(body)) will change byte content
    • Right: Store the raw bytes before parsing
  3. Encoding mismatches: Different platforms encode signatures in different ways (hex vs base64, digest formats vary).

    • Fix: Check the provider's documentation for exact format
    • Stripe: Hex encoding, HMAC-SHA256
    • GitHub: sha256= prefix, hex encoding
    • Shopify: Base64 encoding
    • Slack: sha256= prefix, hex encoding

Timeout Errors

Problem: Your webhook handler is timing out, and the provider retries indefinitely.

Common Causes:

  1. Synchronous processing: Doing all work (database writes, API calls, etc.) inside the webhook handler.
    • Fix: Return a 200 response immediately, queue the work asynchronously:
app.post('/webhooks/stripe', async (req, res) => {
  // Verify signature immediately
  const event = verifyStripeSignature(req);

  // Queue for async processing
  await queue.enqueue('process-stripe-event', event);

  // Return immediately
  res.json({received: true});

  // Process in background without blocking response
});
  1. Network timeouts: Making external API calls inside the webhook handler.

    • Fix: Queue the event and process asynchronously. Add retry logic for external calls.
  2. Database locks: A long-running transaction blocking webhook processing.

    • Fix: Use transactions carefully. Write minimal data in the webhook, queue the rest.

Best Practice: Return 200 within 5 seconds, process everything else asynchronously.

Duplicate Events

Problem: The same event is processed multiple times, causing duplicate charges, double-sent emails, etc.

Causes: Webhooks are delivered "at least once," so duplicates are expected. The solution is idempotency.

Implementation: Idempotency Keys

app.post('/webhooks/stripe', async (req, res) => {
  const event = verifyStripeSignature(req);
  const idempotencyKey = event.id; // Use provider's event ID

  // Check if we've already processed this event
  const existing = await db.webhookEvents.findOne({
    eventId: idempotencyKey,
  });

  if (existing) {
    // Already processed, return success
    return res.json({received: true});
  }

  try {
    // Process event
    if (event.type === 'payment_intent.succeeded') {
      await processPayment(event.data.object);
    }

    // Record successful processing
    await db.webhookEvents.create({
      eventId: idempotencyKey,
      type: event.type,
      processedAt: new Date(),
    });

    res.json({received: true});
  } catch (err) {
    // Don't record on error, let provider retry
    res.status(500).json({error: err.message});
  }
});

Better Option: Database-Backed Deduplication

Use a database with unique constraints:

CREATE TABLE webhook_events (
  id UUID PRIMARY KEY,
  event_id VARCHAR UNIQUE NOT NULL,
  event_type VARCHAR NOT NULL,
  payload JSONB NOT NULL,
  processed BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Insert or ignore
INSERT INTO webhook_events (id, event_id, event_type, payload)
VALUES (gen_random_uuid(), $1, $2, $3)
ON CONFLICT (event_id) DO NOTHING
RETURNING id;

Missing Events

Problem: Some events never arrive, and the order is inconsistent.

Causes: Network failures, provider issues, or your endpoint being down.

Solution: Reconciliation Jobs

Don't rely 100% on webhooks. Run periodic reconciliation:

// Run every 5 minutes
setInterval(async () => {
  // Get last processed webhook timestamp
  const lastEvent = await db.webhookEvents
    .findOne()
    .sort({createdAt: -1});

  const since = lastEvent?.createdAt || new Date(0);

  // Query provider's API for events since that time
  const providerEvents = await stripe.events.list({
    created: {gte: Math.floor(since.getTime() / 1000)},
  });

  // Process any we haven't seen
  for (const event of providerEvents.data) {
    const existing = await db.webhookEvents.findOne({
      eventId: event.id,
    });

    if (!existing) {
      await processWebhookEvent(event);
      await recordEvent(event);
    }
  }
}, 5 * 60 * 1000);

Endpoint Health Monitoring

Track webhook delivery failures:

// In your webhook handler
app.post('/webhooks/stripe', async (req, res) => {
  const event = verifyStripeSignature(req);

  try {
    await processEvent(event);
    res.json({received: true});
  } catch (err) {
    // Log failures for monitoring
    await db.webhookFailures.create({
      eventId: event.id,
      error: err.message,
      timestamp: new Date(),
    });

    // Alert if failure rate exceeds threshold
    const recentFailures = await db.webhookFailures.countDocuments({
      timestamp: {$gte: new Date(Date.now() - 3600000)},
    });

    if (recentFailures > 10) {
      await alertOps('High webhook failure rate detected');
    }

    res.status(500).json({error: err.message});
  }
});

Out-of-Order Delivery

Problem: Events arrive in the wrong order, causing state inconsistencies.

Example: payment_intent.succeeded arrives before invoice.created, but your code expects invoices to exist first.

Solution 1: Timestamp-Based Ordering

Store events and process in timestamp order:

app.post('/webhooks/stripe', async (req, res) => {
  const event = verifyStripeSignature(req);

  // Store event for later processing
  await db.pendingEvents.create({
    eventId: event.id,
    type: event.type,
    timestamp: event.created, // Use provider's timestamp
    payload: event.data.object,
  });

  res.json({received: true});

  // Process in background, ordered by timestamp
  const events = await db.pendingEvents
    .find({processed: false})
    .sort({timestamp: 1})
    .limit(10);

  for (const evt of events) {
    try {
      await processEvent(evt);
      await db.pendingEvents.updateOne({_id: evt._id}, {processed: true});
    } catch (err) {
      console.error('Processing failed:', err);
      // Retry later
    }
  }
});

Solution 2: State Machine Approach

Define valid state transitions:

const VALID_TRANSITIONS = {
  'invoice.created': ['invoice.finalized', 'invoice.deleted'],
  'invoice.finalized': ['payment_intent.created', 'invoice.deleted'],
  'payment_intent.created': ['payment_intent.succeeded', 'payment_intent.payment_failed'],
};

function canProcess(currentState, eventType) {
  return VALID_TRANSITIONS[currentState]?.includes(eventType);
}

app.post('/webhooks/stripe', async (req, res) => {
  const event = verifyStripeSignature(req);
  const resource = event.data.object;

  const subscription = await db.subscriptions.findOne({id: resource.id});

  if (!canProcess(subscription.state, event.type)) {
    // Out of order, queue for retry
    await db.deferredEvents.create({event, retryAt: new Date(Date.now() + 5000)});
    return res.json({received: true});
  }

  // Safe to process
  await processEvent(event);
  res.json({received: true});
});

Common Patterns Across Platforms

Comparison of webhook signature verification methods showing HMAC-SHA256, HMAC-SHA1, ECDSA, and Certificate Chain verification flows from request through validation to processing

Foundational Guides

Webhook Idempotency Guide - Handle duplicate deliveries safely.

HMAC-SHA256 Webhook Signatures - Cryptographic verification for Stripe, Shopify, GitHub, Clerk, and more.

Debugging Webhooks in Production - Logging, replay testing, and health monitoring strategies.


Getting Started with Hook Mesh

Hook Mesh handles webhook infrastructure: automatic retries, signature verification, idempotency, real-time monitoring, replay capabilities, and fan-out delivery.

Start Free Trial | Documentation | Pricing


Keep Learning

This pillar page is continuously updated as we add new platform guides. Bookmark it and check back for coverage of additional platforms including:

  • Zoom webhooks for meeting events
  • Notion webhooks for workspace changes
  • Linear webhooks for issue tracking
  • Intercom webhooks for customer messaging
  • QuickBooks webhooks for accounting sync

Have a platform you'd like us to cover? Let us know.