Migrating from Competitors

A comprehensive guide to migrating your webhook infrastructure to Hook Mesh. Whether you're coming from Stripe Webhooks, Svix, Hookdeck, or a custom solution, this guide will help you transition smoothly with minimal downtime.

Why Migrate to Hook Mesh

Hook Mesh offers compelling advantages over other webhook providers:

Better Reliability

Intelligent circuit breakers, adaptive retry strategies, and 99.9% uptime SLA ensure your webhooks always reach their destination.

Transparent Pricing

Simple per-webhook pricing with no hidden fees. Free tier includes 10,000 webhooks/month. No surprises.

Developer Experience

Clean APIs, comprehensive documentation, and SDKs in every major language. Built by developers, for developers.

Customer Portal

White-labeled UI for your customers to manage their own webhooks. Reduce support tickets by 80%.

Migration Overview

A successful migration follows these key phases to minimize risk and ensure business continuity:

1

Dual-Running Period

Run both Hook Mesh and your existing provider in parallel for 1-2 weeks. Send webhooks to both systems and compare results to validate behavior.

2

Gradual Rollout

Migrate customers gradually by event type or customer segment. Start with test accounts, then expand to 10%, 50%, and finally 100% of traffic.

3

Validation Phase

Monitor success rates, delivery times, and error rates closely. Compare against baseline metrics from your previous provider.

4

Complete Cutover

Once validation is complete, switch 100% of traffic to Hook Mesh. Keep the old system running for 48 hours as a fallback, then decommission.

Migration timeline: Plan for 2-4 weeks from start to finish. Rushing the migration increases risk of customer-facing issues.

Feature Comparison

Compare Hook Mesh against other popular webhook providers:

FeatureHook MeshStripe WebhooksSvixHookdeckInngest
Delivery GuaranteeAt-least-onceAt-least-onceAt-least-onceAt-least-onceAt-least-once
Retry StrategyExponential backoffExponential backoffExponential backoffConfigurableExponential backoff
Circuit Breaker✅ Built-in❌ No❌ No✅ Yes❌ No
Customer Portal✅ Built-in❌ No✅ Yes✅ Yes❌ No
Pricing ModelPer webhookBundled with StripePer messagePer eventPer step
Free Tier10K/monthN/A50K/month100K/monthLimited
Self-Hosting✅ Available❌ No✅ Yes❌ No✅ Yes
Signature AlgorithmHMAC-SHA256HMAC-SHA256Ed25519HMAC-SHA256HMAC-SHA256
Rate Limits10K req/secVaries1K req/secVariesVaries

From Stripe Webhooks

Migrating from Stripe's webhook system is straightforward. The main differences are in signature verification and endpoint management.

Key Differences

AspectStripeHook Mesh
Signature HeaderStripe-Signaturewebhook-signature
Signature Formatt=timestamp,v1=sig,v0=sigv1,base64_signature
Timestamp HeaderIn Stripe-Signaturewebhook-timestamp
Event ID HeaderIn payloadwebhook-id
Endpoint ManagementStripe DashboardAPI + Customer Portal

API Endpoint Mapping

OperationStripeHook Mesh
Create EndpointPOST /v1/webhook_endpointsPOST /v1/endpoints
Send WebhookN/A (automatic)POST /v1/webhook-jobs
List EventsGET /v1/eventsGET /v1/webhook-jobs
Retry EventManual via dashboardPOST /v1/webhook-jobs/:id/retry

Converting Signature Verification

Here's how to update your webhook handler from Stripe to Hook Mesh:

Stripe Webhook Handler (Before)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;
  try {
    // Stripe signature verification
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log(`Webhook signature verification failed: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (event.type) {
    case 'customer.created':
      const customer = event.data.object;
      handleCustomerCreated(customer);
      break;
    case 'payment_intent.succeeded':
      const paymentIntent = event.data.object;
      handlePaymentSuccess(paymentIntent);
      break;
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  res.json({ received: true });
});
Hook Mesh Webhook Handler (After)
const crypto = require('crypto');

function verifyHookMeshSignature(payload, headers, secret) {
  const webhookId = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signature = headers['webhook-signature'];

  if (!webhookId || !timestamp || !signature) {
    throw new Error('Missing required headers');
  }

  // Extract signature (format: "v1,<signature>")
  const [version, expectedSig] = signature.split(',');
  if (version !== 'v1') {
    throw new Error('Unsupported signature version');
  }

  // Construct signed content: {webhookId}.{timestamp}.{jsonPayload}
  const signedContent = `${webhookId}.${timestamp}.${JSON.stringify(payload)}`;

  // Compute HMAC-SHA256 and encode as base64
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(signedContent);
  const computedSig = hmac.digest('base64');

  // Timing-safe comparison
  if (!crypto.timingSafeEqual(Buffer.from(expectedSig), Buffer.from(computedSig))) {
    throw new Error('Invalid signature');
  }

  // Verify timestamp (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  const webhookTime = parseInt(timestamp);
  if (Math.abs(now - webhookTime) > 300) {
    throw new Error('Webhook timestamp too old or too far in future');
  }

  return true;
}

app.post('/webhooks/hookmesh', express.json(), (req, res) => {
  try {
    // Hook Mesh signature verification
    verifyHookMeshSignature(
      req.body,
      req.headers,
      process.env.HOOKMESH_WEBHOOK_SECRET
    );
  } catch (err) {
    console.log(`Webhook signature verification failed: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (req.body.event_type) {
    case 'customer.created':
      handleCustomerCreated(req.body);
      break;
    case 'payment.succeeded':
      handlePaymentSuccess(req.body);
      break;
    default:
      console.log(`Unhandled event type ${req.body.event_type}`);
  }

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

From Svix

Svix uses a similar architecture to Hook Mesh, making migration straightforward. The main differences are in signature verification and terminology.

Terminology Mapping

Svix TermHook Mesh Equivalent
ApplicationApplication
EndpointEndpoint
MessageWebhook Job
Event TypeEvent Type
App PortalCustomer Portal

Signature Differences

Svix uses Ed25519 signatures, while Hook Mesh uses HMAC-SHA256. Update your verification code:

Svix Signature Verification (Before)
from svix.webhooks import Webhook, WebhookVerificationError

@app.route('/webhooks/svix', methods=['POST'])
def handle_svix_webhook():
    payload = request.get_data()
    headers = request.headers
    secret = os.environ['SVIX_WEBHOOK_SECRET']

    try:
        # Svix uses Ed25519 signatures
        wh = Webhook(secret)
        msg = wh.verify(payload, headers)
    except WebhookVerificationError as e:
        return jsonify({'error': 'Invalid signature'}), 400

    # Process event
    event_type = msg['eventType']
    if event_type == 'user.created':
        handle_user_created(msg['data'])

    return jsonify({'success': True})
Hook Mesh Signature Verification (After)
import hmac
import hashlib
import base64
import json
import time

def verify_hookmesh_signature(payload, headers, secret):
    webhook_id = headers.get('webhook-id')
    timestamp = headers.get('webhook-timestamp')
    signature = headers.get('webhook-signature')

    if not webhook_id or not timestamp or not signature:
        raise ValueError('Missing required headers')

    # Extract signature (format: "v1,<signature>")
    version, expected_sig = signature.split(',', 1)
    if version != 'v1':
        raise ValueError('Unsupported signature version')

    # Construct signed content: {webhookId}.{timestamp}.{jsonPayload}
    signed_content = f"{webhook_id}.{timestamp}.{payload}"

    # Compute HMAC-SHA256 and encode as base64
    computed_sig = hmac.new(
        secret.encode('utf-8'),
        signed_content.encode('utf-8'),
        hashlib.sha256
    ).digest()
    computed_sig_b64 = base64.b64encode(computed_sig).decode('utf-8')

    # Timing-safe comparison
    if not hmac.compare_digest(expected_sig, computed_sig_b64):
        raise ValueError('Invalid signature')

    # Verify timestamp (prevent replay attacks)
    now = int(time.time())
    webhook_time = int(timestamp)
    if abs(now - webhook_time) > 300:
        raise ValueError('Webhook timestamp invalid')

@app.route('/webhooks/hookmesh', methods=['POST'])
def handle_hookmesh_webhook():
    payload = request.get_data().decode('utf-8')
    headers = request.headers
    secret = os.environ['HOOKMESH_WEBHOOK_SECRET']

    try:
        verify_hookmesh_signature(payload, headers, secret)
    except ValueError as e:
        return jsonify({'error': str(e)}), 400

    # Process event
    data = json.loads(payload)
    event_type = data['event_type']
    if event_type == 'user.created':
        handle_user_created(data)

    return jsonify({'success': True})

API Migration Example

Migrating from Svix to Hook Mesh API
// Before: Svix API
import { Svix } from 'svix';

const svix = new Svix(process.env.SVIX_API_KEY);

// Create endpoint
const endpoint = await svix.endpoint.create('app_123', {
  url: 'https://example.com/webhooks',
  eventTypes: ['user.created', 'order.completed']
});

// Send message
const message = await svix.message.create('app_123', {
  eventType: 'user.created',
  payload: {
    user_id: 'usr_123',
    email: 'user@example.com'
  }
});

// ---

// After: Hook Mesh API
const apiKey = process.env.HOOKMESH_API_KEY;
const baseUrl = 'https://api.hookmesh.com/v1';

// Create endpoint
const endpoint = await fetch(`${baseUrl}/endpoints`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    application_id: 'app_abc123',
    url: 'https://example.com/webhooks',
    event_types: ['user.created', 'order.completed']
  })
}).then(r => r.json());

// Send webhook
const webhook = await fetch(`${baseUrl}/webhook-jobs`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    application_id: 'app_abc123',
    event_type: 'user.created',
    payload: {
      user_id: 'usr_123',
      email: 'user@example.com'
    }
  })
}).then(r => r.json());

From Hookdeck

Hookdeck focuses on webhook ingestion and transformation. Hook Mesh focuses on outbound delivery. Migration requires restructuring your webhook flow.

Architectural Differences

Hookdeck Model

Source (incoming webhooks)
Connection (routing rules)
Destination (your endpoint)
Transformation (optional)

Hook Mesh Model

Application (your product)
Endpoint (customer's URL)
Webhook Job (outbound event)
Event Type (categorization)
Migrating from Hookdeck to Hook Mesh
// Before: Sending to Hookdeck (ingestion model)
// Your app sends webhooks to Hookdeck, which forwards them
func sendViaHookdeck(event Event) error {
    // Send to Hookdeck ingestion endpoint
    resp, err := http.Post(
        "https://events.hookdeck.com/e/src_abc123",
        "application/json",
        bytes.NewBuffer(eventJSON),
    )
    // Hookdeck handles routing and delivery
    return err
}

// ---

// After: Sending via Hook Mesh (direct delivery model)
// Your app calls Hook Mesh API to deliver directly to customer endpoints
func sendViaHookMesh(event Event) error {
    payload := map[string]interface{}{
        "application_id": "app_abc123",
        "event_type":     event.Type,
        "payload":        event.Data,
    }

    payloadJSON, _ := json.Marshal(payload)

    req, _ := http.NewRequest(
        "POST",
        "https://api.hookmesh.com/v1/webhook-jobs",
        bytes.NewBuffer(payloadJSON),
    )

    req.Header.Set("Authorization", "Bearer "+os.Getenv("HOOKMESH_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != 201 {
        return fmt.Errorf("webhook job creation failed: %d", resp.StatusCode)
    }

    return nil
}

From Custom Solution

Many companies build custom webhook delivery systems. Migrating to Hook Mesh eliminates maintenance burden while improving reliability.

Benefits of Switching

✅ What You Gain

  • Intelligent retry strategies (exponential backoff)
  • Circuit breaker protection (auto-pause bad endpoints)
  • Customer portal (self-service endpoint management)
  • Detailed delivery analytics and monitoring
  • Zero maintenance (we handle infrastructure)
  • Compliance-ready (SOC 2, GDPR)

❌ What You Eliminate

  • Queue infrastructure management (Redis/RabbitMQ)
  • Retry logic bugs and edge cases
  • Scaling challenges during traffic spikes
  • Customer support tickets for webhook issues
  • Database tables for webhook logs and status
  • Monitoring and alerting setup

Common Custom Solution Patterns

Typical Custom Webhook Sender (Before)
// Most custom solutions follow this pattern:
const Redis = require('ioredis');
const axios = require('axios');

const redis = new Redis();

// 1. Queue webhook for delivery
async function queueWebhook(customerId, eventType, payload) {
  const webhook = {
    id: generateId(),
    customer_id: customerId,
    event_type: eventType,
    payload: payload,
    attempts: 0,
    created_at: Date.now()
  };

  // Add to Redis queue
  await redis.lpush('webhook:pending', JSON.stringify(webhook));

  // Store in database
  await db.webhooks.insert(webhook);
}

// 2. Worker process to send webhooks
async function webhookWorker() {
  while (true) {
    // Pop from queue
    const item = await redis.brpop('webhook:pending', 5);
    if (!item) continue;

    const webhook = JSON.parse(item[1]);

    try {
      // Get customer endpoint
      const endpoint = await db.endpoints.findOne({
        customer_id: webhook.customer_id
      });

      // Send webhook
      const response = await axios.post(endpoint.url, webhook.payload, {
        headers: {
          'X-Event-Type': webhook.event_type,
          'X-Signature': generateSignature(webhook.payload, endpoint.secret)
        },
        timeout: 10000
      });

      // Mark as delivered
      await db.webhooks.update(webhook.id, {
        status: 'delivered',
        response_status: response.status
      });

    } catch (error) {
      // Retry logic (buggy and complex)
      webhook.attempts++;

      if (webhook.attempts < 5) {
        // Exponential backoff (manual calculation)
        const delay = Math.pow(2, webhook.attempts) * 1000;
        await redis.zadd('webhook:retry', Date.now() + delay, JSON.stringify(webhook));
      } else {
        // Give up after 5 attempts
        await db.webhooks.update(webhook.id, {
          status: 'failed',
          error: error.message
        });
      }
    }
  }
}

// 3. Retry scheduler
async function retryScheduler() {
  while (true) {
    const now = Date.now();
    const items = await redis.zrangebyscore('webhook:retry', 0, now, 'LIMIT', 0, 10);

    for (const item of items) {
      await redis.lpush('webhook:pending', item);
      await redis.zrem('webhook:retry', item);
    }

    await sleep(1000);
  }
}

// Run workers
webhookWorker();
retryScheduler();
Hook Mesh Replacement (After)
// Replace entire custom solution with simple API call:
const axios = require('axios');

async function sendWebhook(customerId, eventType, payload) {
  try {
    const response = await axios.post(
      'https://api.hookmesh.com/v1/webhook-jobs',
      {
        application_id: process.env.HOOKMESH_APP_ID,
        customer_id: customerId,
        event_type: eventType,
        payload: payload
      },
      {
        headers: {
          'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
          'Content-Type': 'application/json'
        }
      }
    );

    // Hook Mesh handles:
    // - Finding customer endpoints
    // - Signature generation
    // - Delivery with retries
    // - Circuit breaker logic
    // - Status tracking
    // - Customer notifications

    return response.data;
  } catch (error) {
    console.error('Failed to queue webhook:', error.message);
    throw error;
  }
}

// That's it! No workers, queues, or retry logic needed.
// Use Hook Mesh API to query delivery status:
async function getWebhookStatus(jobId) {
  const response = await axios.get(
    `https://api.hookmesh.com/v1/webhook-jobs/${jobId}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`
      }
    }
  );

  return response.data;
  // Returns: { status: 'delivered', attempts: 1, ... }
}
Maintenance reduction: Companies typically reduce webhook-related code by 80-90% and eliminate 2-3 backend services when migrating from custom solutions.

Detailed Migration Steps

Follow these steps for a smooth migration with minimal risk:

Step 1: Set Up Hook Mesh

Create your Hook Mesh account and configure your first application:

Create Hook Mesh Account
# 1. Sign up at https://app.hookmesh.com/signup

# 2. Get your API key from the dashboard

# 3. Set environment variable
export HOOKMESH_API_KEY="sk_live_your_api_key_here"
Create Application (TypeScript)
const response = await fetch('https://api.hookmesh.com/v1/applications', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'My Application',
    description: 'Production webhook delivery'
  })
});

const application = await response.json();
console.log('Application ID:', application.id);
// Save this ID for subsequent API calls
Create Application (Python)
import requests
import os

response = requests.post(
    'https://api.hookmesh.com/v1/applications',
    headers={
        'Authorization': f'Bearer {os.environ["HOOKMESH_API_KEY"]}',
        'Content-Type': 'application/json'
    },
    json={
        'name': 'My Application',
        'description': 'Production webhook delivery'
    }
)

application = response.json()
print(f'Application ID: {application["id"]}')
Create Application (Go)
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func createApplication() (string, error) {
    payload := map[string]string{
        "name":        "My Application",
        "description": "Production webhook delivery",
    }

    payloadJSON, _ := json.Marshal(payload)

    req, _ := http.NewRequest(
        "POST",
        "https://api.hookmesh.com/v1/applications",
        bytes.NewBuffer(payloadJSON),
    )

    req.Header.Set("Authorization", "Bearer "+os.Getenv("HOOKMESH_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)

    return result["id"].(string), nil
}
Create Application (PHP)
<?php

$apiKey = getenv('HOOKMESH_API_KEY');

$ch = curl_init('https://api.hookmesh.com/v1/applications');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'name' => 'My Application',
    'description' => 'Production webhook delivery'
]));

$response = curl_exec($ch);
$application = json_decode($response, true);

echo "Application ID: " . $application['id'] . "
";
curl_close($ch);

Step 2: Migrate Endpoints

Export endpoints from your existing provider and import them to Hook Mesh:

Bulk Endpoint Migration Script
import fs from 'fs';

// 1. Export endpoints from existing provider
// (Use their API or export feature)
const existingEndpoints = [
  {
    customer_id: 'cust_123',
    url: 'https://customer1.com/webhooks',
    event_types: ['user.created', 'order.completed']
  },
  {
    customer_id: 'cust_456',
    url: 'https://customer2.com/webhooks',
    event_types: ['user.created']
  }
  // ... more endpoints
];

// 2. Bulk import to Hook Mesh
async function migrateEndpoints(endpoints: any[]) {
  const results = {
    success: 0,
    failed: 0,
    errors: [] as any[]
  };

  for (const endpoint of endpoints) {
    try {
      const response = await fetch('https://api.hookmesh.com/v1/endpoints', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          application_id: process.env.HOOKMESH_APP_ID,
          customer_id: endpoint.customer_id,
          url: endpoint.url,
          event_types: endpoint.event_types,
          description: `Migrated from existing provider`
        })
      });

      if (response.ok) {
        results.success++;
        const created = await response.json();
        console.log(`✅ Migrated endpoint for ${endpoint.customer_id}: ${created.id}`);
      } else {
        results.failed++;
        const error = await response.json();
        results.errors.push({
          customer_id: endpoint.customer_id,
          error: error.message
        });
        console.error(`❌ Failed to migrate ${endpoint.customer_id}: ${error.message}`);
      }

      // Rate limiting: wait 100ms between requests
      await new Promise(resolve => setTimeout(resolve, 100));

    } catch (error) {
      results.failed++;
      results.errors.push({
        customer_id: endpoint.customer_id,
        error: error.message
      });
      console.error(`❌ Error migrating ${endpoint.customer_id}:`, error);
    }
  }

  // 3. Save migration report
  fs.writeFileSync('migration-report.json', JSON.stringify(results, null, 2));

  console.log(`
--- Migration Complete ---`);
  console.log(`✅ Successful: ${results.success}`);
  console.log(`❌ Failed: ${results.failed}`);

  if (results.errors.length > 0) {
    console.log(`
Errors saved to migration-report.json`);
  }

  return results;
}

// Run migration
migrateEndpoints(existingEndpoints);

Step 3: Update Webhook Handlers

Update your customers' webhook receivers to verify Hook Mesh signatures:

Side-by-Side: Old vs New Handler
// ===== BEFORE (Example: Stripe) =====
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  handleEvent(event.type, event.data.object);
  res.json({ received: true });
});

// ===== AFTER (Hook Mesh) =====
app.post('/webhooks/hookmesh', express.json(), (req, res) => {
  try {
    // 1. Verify Hook Mesh signature
    verifyHookMeshSignature(req.body, req.headers, process.env.HOOKMESH_SECRET);

    // 2. Handle event (same business logic)
    handleEvent(req.body.event_type, req.body.payload);

    res.json({ received: true });
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
});

// Helper function (add this)
function verifyHookMeshSignature(payload, headers, secret) {
  const crypto = require('crypto');

  const webhookId = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signature = headers['webhook-signature'];

  if (!webhookId || !timestamp || !signature) {
    throw new Error('Missing required headers');
  }

  const [version, expectedSig] = signature.split(',');
  if (version !== 'v1') {
    throw new Error('Unsupported signature version');
  }

  const signedContent = `${webhookId}.${timestamp}.${JSON.stringify(payload)}`;
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(signedContent);
  const computedSig = hmac.digest('base64');

  if (!crypto.timingSafeEqual(Buffer.from(expectedSig), Buffer.from(computedSig))) {
    throw new Error('Invalid signature');
  }

  const now = Math.floor(Date.now() / 1000);
  const webhookTime = parseInt(timestamp);
  if (Math.abs(now - webhookTime) > 300) {
    throw new Error('Webhook timestamp too old or too far in future');
  }
}

Step 4: Dual-Running

Send webhooks to both systems simultaneously to validate Hook Mesh behavior:

Dual-Send Pattern with Comparison
async function sendWebhookDualMode(customerId: string, eventType: string, payload: any) {
  const results = {
    oldProvider: null as any,
    hookMesh: null as any,
    comparison: {} as any
  };

  // Send to both providers in parallel
  const [oldResult, hookMeshResult] = await Promise.allSettled([
    // Old provider (e.g., Stripe, Svix)
    sendViaOldProvider(customerId, eventType, payload),

    // Hook Mesh
    sendViaHookMesh(customerId, eventType, payload)
  ]);

  // Record results
  if (oldResult.status === 'fulfilled') {
    results.oldProvider = { status: 'success', data: oldResult.value };
  } else {
    results.oldProvider = { status: 'failed', error: oldResult.reason };
  }

  if (hookMeshResult.status === 'fulfilled') {
    results.hookMesh = { status: 'success', data: hookMeshResult.value };
  } else {
    results.hookMesh = { status: 'failed', error: hookMeshResult.reason };
  }

  // Compare results
  results.comparison = {
    bothSucceeded: results.oldProvider.status === 'success' && results.hookMesh.status === 'success',
    bothFailed: results.oldProvider.status === 'failed' && results.hookMesh.status === 'failed',
    onlyOldSucceeded: results.oldProvider.status === 'success' && results.hookMesh.status === 'failed',
    onlyHookMeshSucceeded: results.oldProvider.status === 'failed' && results.hookMesh.status === 'success'
  };

  // Log discrepancies
  if (results.comparison.onlyOldSucceeded || results.comparison.onlyHookMeshSucceeded) {
    console.warn('⚠️  Delivery discrepancy detected:', {
      customer_id: customerId,
      event_type: eventType,
      comparison: results.comparison
    });

    // Alert your team
    await alertTeam({
      type: 'webhook_discrepancy',
      customer_id: customerId,
      results: results
    });
  }

  // Store comparison data for analysis
  await db.migration_logs.insert({
    customer_id: customerId,
    event_type: eventType,
    timestamp: new Date(),
    results: results
  });

  // Return Hook Mesh result (we're migrating to it)
  return results.hookMesh;
}

async function sendViaHookMesh(customerId: string, eventType: string, payload: any) {
  const response = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      application_id: process.env.HOOKMESH_APP_ID,
      customer_id: customerId,
      event_type: eventType,
      payload: payload
    })
  });

  return response.json();
}

Step 5: Gradual Cutover

Use feature flags to gradually migrate traffic to Hook Mesh:

Feature Flag Implementation
// Use a feature flag service (LaunchDarkly, ConfigCat, etc.)
// or implement a simple database-backed flag

async function sendWebhook(customerId: string, eventType: string, payload: any) {
  // Check if customer should use Hook Mesh
  const useHookMesh = await shouldUseHookMesh(customerId);

  if (useHookMesh) {
    console.log(`📤 Sending via Hook Mesh for customer ${customerId}`);
    return sendViaHookMesh(customerId, eventType, payload);
  } else {
    console.log(`📤 Sending via old provider for customer ${customerId}`);
    return sendViaOldProvider(customerId, eventType, payload);
  }
}

// Feature flag logic
async function shouldUseHookMesh(customerId: string): Promise<boolean> {
  // Option 1: Percentage rollout
  const rolloutPercentage = await getConfigValue('hookmesh_rollout_percentage');
  const customerHash = hashCustomerId(customerId);
  if (customerHash % 100 < rolloutPercentage) {
    return true;
  }

  // Option 2: Explicit allowlist (test customers)
  const allowlist = await getConfigValue('hookmesh_allowlist');
  if (allowlist.includes(customerId)) {
    return true;
  }

  // Option 3: Event type rollout
  const enabledEventTypes = await getConfigValue('hookmesh_event_types');
  if (enabledEventTypes.includes(eventType)) {
    return true;
  }

  return false;
}

// Gradually increase rollout percentage:
// Day 1: 0% (dual-send only)
// Day 3: 10% (test customers)
// Day 5: 25% (expanding)
// Day 7: 50% (half of traffic)
// Day 10: 100% (complete migration)

Step 6: Complete Migration

Once validation is complete and 100% of traffic is on Hook Mesh:

Final Cutover Checklist

  1. Verify 100% of webhooks are successfully delivering via Hook Mesh for 48+ hours
  2. Compare delivery success rates between old provider and Hook Mesh (should be equal or better)
  3. Confirm no webhook-related support tickets from customers
  4. Remove dual-send code and feature flags
  5. Keep old provider active for 48 hours as emergency fallback
  6. Cancel old provider subscription and delete data
  7. Update documentation and runbooks
  8. Notify team that migration is complete

Testing Strategy

Thoroughly test your Hook Mesh integration before going live:

Migration Testing Checklist

Test endpoint creation via API
Test webhook sending for all event types
Verify signature verification works correctly
Test retry behavior (simulate endpoint failures)
Test idempotency (send duplicate webhooks, verify handler behavior)
Test circuit breaker (verify endpoints pause after repeated failures)
Load test (send 10K+ webhooks, verify no dropped deliveries)
Test customer portal (verify customers can manage endpoints)
Test secret rotation (zero-downtime rotation)
Monitor dual-send comparison logs (verify consistent behavior)
Load Testing Script
#!/bin/bash

# Load test Hook Mesh integration
# Sends 10,000 test webhooks and verifies delivery

echo "Starting Hook Mesh load test..."

SUCCESSFUL=0
FAILED=0

for i in {1..10000}; do
  RESPONSE=$(curl -s -w "%{http_code}" -o /dev/null     -X POST https://api.hookmesh.com/v1/webhook-jobs     -H "Authorization: Bearer $HOOKMESH_API_KEY"     -H "Content-Type: application/json"     -d '{
      "application_id": "'"$HOOKMESH_APP_ID"'",
      "event_type": "load_test.event",
      "payload": {
        "test_id": "'"$i"'",
        "timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"
      }
    }')

  if [ "$RESPONSE" -eq 201 ]; then
    ((SUCCESSFUL++))
  else
    ((FAILED++))
    echo "❌ Request $i failed with status $RESPONSE"
  fi

  # Progress indicator every 1000 requests
  if [ $((i % 1000)) -eq 0 ]; then
    echo "✅ Sent $i webhooks... (Success: $SUCCESSFUL, Failed: $FAILED)"
  fi
done

echo ""
echo "--- Load Test Complete ---"
echo "✅ Successful: $SUCCESSFUL"
echo "❌ Failed: $FAILED"
echo "Success Rate: $(echo "scale=2; $SUCCESSFUL * 100 / 10000" | bc)%"

Common Migration Issues

Troubleshoot common problems encountered during migration:

IssueCauseSolution
Signature verification failsUsing wrong header names or signature formatUpdate to Hook Mesh header format: webhook-signature
Webhooks not deliveringEndpoints not created or wrong application_idVerify endpoints exist via API: GET /v1/endpoints
Event type mismatchUsing old provider's event namingMap event types: payment_intent.succeeded payment.succeeded
Rate limit errorsSending too many webhooks concurrentlyImplement rate limiting in your code or contact support for higher limits
Retry behavior differentHook Mesh uses different backoff scheduleReview Hook Mesh retry strategy and adjust expectations
Payload structure changedOld provider wrapped payload in extra fieldsUpdate handlers to expect Hook Mesh payload format
Need help? Contact our migration team at migrations@hookmesh.com for personalized assistance with your migration.

Rollback Plan

Always have a rollback plan in case of issues during migration:

Pre-Migration Preparation

  • Keep old provider account active during migration period
  • Document current webhook delivery baseline metrics
  • Maintain access to old provider API keys and credentials
  • Back up endpoint configurations from old provider
  • Set up dual-send mode before switching any traffic

Quick Rollback Procedure

  1. Immediately set feature flag to 0% Hook Mesh traffic
  2. Verify all traffic is back on old provider (check logs)
  3. Notify team and customers (if customer-facing issues occurred)
  4. Investigate root cause of issues
  5. Fix issues and re-test before attempting migration again
  6. Document lessons learned and update migration runbook

Rollback Triggers

Initiate rollback if you observe:

  • Delivery success rate drops below 95% (vs 99%+ baseline)
  • Customer reports of missing webhooks (2+ reports in 1 hour)
  • Signature verification failure rate above 1%
  • P0/P1 incidents related to webhook delivery
  • Circuit breaker triggering for > 10% of endpoints
Emergency Rollback Script
// Keep this script handy for quick rollback

async function emergencyRollback() {
  console.log('🚨 INITIATING EMERGENCY ROLLBACK 🚨');

  // 1. Set feature flag to 0%
  await setConfigValue('hookmesh_rollout_percentage', 0);
  console.log('✅ Feature flag set to 0%');

  // 2. Clear any caches
  await clearConfigCache();
  console.log('✅ Config cache cleared');

  // 3. Verify rollback
  const currentPercentage = await getConfigValue('hookmesh_rollout_percentage');
  if (currentPercentage !== 0) {
    throw new Error('❌ Rollback failed! Percentage is still ' + currentPercentage);
  }

  console.log('✅ Rollback complete. All traffic back on old provider.');

  // 4. Alert team
  await alertTeam({
    type: 'migration_rollback',
    message: 'Hook Mesh migration rolled back to 0%',
    timestamp: new Date()
  });

  // 5. Monitor for 30 minutes
  console.log('📊 Monitoring traffic for 30 minutes...');
  await monitorTraffic(30);

  console.log('✅ Rollback confirmed successful.');
}

// Usage: node rollback.js
emergencyRollback().catch(console.error);

Migration Checklist

Complete this checklist to ensure a successful migration:

Complete Migration Checklist

✅ Hook Mesh account created and API key obtained
✅ Applications created in Hook Mesh
✅ All customer endpoints migrated to Hook Mesh
✅ Webhook handlers updated with Hook Mesh signature verification
✅ Signature verification tested and working correctly
✅ Dual-running validated (both systems delivering successfully)
✅ Feature flags implemented for gradual rollout
✅ Monitoring and alerting configured
✅ Team trained on Hook Mesh dashboard and APIs
✅ Documentation updated (runbooks, API docs, customer guides)
✅ Rollback plan documented and tested
✅ Customer communication prepared (if needed)
✅ Load testing completed (10K+ webhooks)
✅ Gradual rollout completed (0% → 10% → 50% → 100%)
✅ 48-hour monitoring period completed successfully
✅ Old provider decommissioned and subscription cancelled

Migration Support

We're here to help you succeed with your migration:

Migration Assistance

Our team can help with planning, implementation, and troubleshooting.

migrations@hookmesh.com

Technical Review

Schedule a call to review your migration plan and architecture.

Schedule 30-minute call

Dedicated Slack Channel

Get real-time help during your migration in a private Slack channel.

Request Slack access

Priority Support

Upgrade to Priority Support for SLA-backed response times during migration.

View pricing
Migration guarantee: We're committed to your success. If you encounter any issues during migration, our team will work with you until they're resolved.

Next Steps

Ready to start your migration? Here's what to do next: