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:
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.
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.
Validation Phase
Monitor success rates, delivery times, and error rates closely. Compare against baseline metrics from your previous provider.
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.
Feature Comparison
Compare Hook Mesh against other popular webhook providers:
| Feature | Hook Mesh | Stripe Webhooks | Svix | Hookdeck | Inngest |
|---|---|---|---|---|---|
| Delivery Guarantee | At-least-once | At-least-once | At-least-once | At-least-once | At-least-once |
| Retry Strategy | Exponential backoff | Exponential backoff | Exponential backoff | Configurable | Exponential backoff |
| Circuit Breaker | ✅ Built-in | ❌ No | ❌ No | ✅ Yes | ❌ No |
| Customer Portal | ✅ Built-in | ❌ No | ✅ Yes | ✅ Yes | ❌ No |
| Pricing Model | Per webhook | Bundled with Stripe | Per message | Per event | Per step |
| Free Tier | 10K/month | N/A | 50K/month | 100K/month | Limited |
| Self-Hosting | ✅ Available | ❌ No | ✅ Yes | ❌ No | ✅ Yes |
| Signature Algorithm | HMAC-SHA256 | HMAC-SHA256 | Ed25519 | HMAC-SHA256 | HMAC-SHA256 |
| Rate Limits | 10K req/sec | Varies | 1K req/sec | Varies | Varies |
From Stripe Webhooks
Migrating from Stripe's webhook system is straightforward. The main differences are in signature verification and endpoint management.
Key Differences
| Aspect | Stripe | Hook Mesh |
|---|---|---|
| Signature Header | Stripe-Signature | webhook-signature |
| Signature Format | t=timestamp,v1=sig,v0=sig | v1,base64_signature |
| Timestamp Header | In Stripe-Signature | webhook-timestamp |
| Event ID Header | In payload | webhook-id |
| Endpoint Management | Stripe Dashboard | API + Customer Portal |
API Endpoint Mapping
| Operation | Stripe | Hook Mesh |
|---|---|---|
| Create Endpoint | POST /v1/webhook_endpoints | POST /v1/endpoints |
| Send Webhook | N/A (automatic) | POST /v1/webhook-jobs |
| List Events | GET /v1/events | GET /v1/webhook-jobs |
| Retry Event | Manual via dashboard | POST /v1/webhook-jobs/:id/retry |
Converting Signature Verification
Here's how to update your webhook handler from Stripe to Hook Mesh:
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 });
});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 Term | Hook Mesh Equivalent |
|---|---|
| Application | Application |
| Endpoint | Endpoint |
| Message | Webhook Job |
| Event Type | Event Type |
| App Portal | Customer Portal |
Signature Differences
Svix uses Ed25519 signatures, while Hook Mesh uses HMAC-SHA256. Update your verification code:
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})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
// 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
Hook Mesh Model
// 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
// 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();// 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, ... }
}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:
# 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"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 callsimport 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"]}')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
}<?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:
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:
// ===== 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:
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:
// 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
- Verify 100% of webhooks are successfully delivering via Hook Mesh for 48+ hours
- Compare delivery success rates between old provider and Hook Mesh (should be equal or better)
- Confirm no webhook-related support tickets from customers
- Remove dual-send code and feature flags
- Keep old provider active for 48 hours as emergency fallback
- Cancel old provider subscription and delete data
- Update documentation and runbooks
- 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) |
#!/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:
| Issue | Cause | Solution |
|---|---|---|
| Signature verification fails | Using wrong header names or signature format | Update to Hook Mesh header format: webhook-signature |
| Webhooks not delivering | Endpoints not created or wrong application_id | Verify endpoints exist via API: GET /v1/endpoints |
| Event type mismatch | Using old provider's event naming | Map event types: payment_intent.succeeded → payment.succeeded |
| Rate limit errors | Sending too many webhooks concurrently | Implement rate limiting in your code or contact support for higher limits |
| Retry behavior different | Hook Mesh uses different backoff schedule | Review Hook Mesh retry strategy and adjust expectations |
| Payload structure changed | Old provider wrapped payload in extra fields | Update handlers to expect Hook Mesh payload format |
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
- Immediately set feature flag to 0% Hook Mesh traffic
- Verify all traffic is back on old provider (check logs)
- Notify team and customers (if customer-facing issues occurred)
- Investigate root cause of issues
- Fix issues and re-test before attempting migration again
- 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
// 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.comTechnical Review
Schedule a call to review your migration plan and architecture.
Schedule 30-minute callDedicated Slack Channel
Get real-time help during your migration in a private Slack channel.
Request Slack accessPriority Support
Upgrade to Priority Support for SLA-backed response times during migration.
View pricingNext Steps
Ready to start your migration? Here's what to do next:
1. Quick Start Guide
Set up your first Hook Mesh application and send your first webhook in 5 minutes.
2. API Reference
Complete API documentation for creating endpoints, sending webhooks, and managing your applications.
3. Signature Verification
Learn how to verify Hook Mesh webhook signatures in Node.js, Python, Go, and PHP.
4. Customer Portal Setup
Enable the white-labeled customer portal so your users can manage their own webhook endpoints.