Svix vs Hookdeck vs Convoy: Webhook Infrastructure Platforms 2026
Svix vs Hookdeck vs Convoy: Webhook Infrastructure Platforms 2026
TL;DR
Svix is the best choice if you're building a product that sends webhooks to customers — it provides a full embeddable portal, automatic retries, and multi-tenant management out of the box. Hookdeck is purpose-built for receiving and routing incoming webhooks reliably — ideal for teams tired of losing events from Stripe, GitHub, or any third-party service. Convoy is the only fully open-source option among the three, giving you complete control when self-hosting is non-negotiable.
Key Takeaways
- Svix dominates outbound webhook use cases — SDKs for 12+ languages, an embeddable portal for your customers to manage subscriptions, and enterprise features like message filtering
- Hookdeck excels at inbound webhook orchestration — rate limiting, fan-out, transformations, replays, and dead-letter queues without writing infra code
- Convoy is fully open source (Apache 2.0) — the only choice when you need full data ownership or on-prem deployment with no vendor lock-in
- Svix GitHub stars: ~3.4k (Jan 2026) — strong growth in the B2B SaaS space
- Hookdeck GitHub stars: ~1.7k — focused tool with a loyal DevOps/platform engineering following
- Convoy GitHub stars: ~4.2k — open-source momentum with active community contributions
- Reliability floor: All three provide automatic retries with exponential backoff; Svix and Hookdeck add observability and alerting; Convoy requires you to configure your own monitoring
Why Webhook Infrastructure Is a Real Problem
Webhooks seem simple — POST a JSON payload to a URL — but production webhook systems are notoriously hard to get right. Three things kill teams:
- Silent failures — the endpoint returns 500, nobody notices, events are lost
- Thundering herds — a delayed consumer causes a backlog that overwhelms your sender
- Debugging nightmares — figuring out which events fired, in what order, and why they failed requires digging through logs that may not exist
Dedicated webhook infrastructure platforms handle all three. They've become a distinct category in the developer tooling stack, alongside message queues (Kafka, NATS) and job queues (BullMQ, Inngest). Choosing the right one depends on whether your primary concern is sending webhooks (outbound) or receiving them (inbound).
Svix: The Gold Standard for Outbound Webhooks
Svix is designed for companies that need to send webhooks to their customers — think Stripe's webhook system, but as a managed service you can embed in your own product.
Core Integration
import { Svix } from "svix";
const svix = new Svix(process.env.SVIX_AUTH_TOKEN!);
// Create an application (one per customer/tenant)
const app = await svix.application.create({
name: "Acme Corp",
uid: "acme-corp-12345", // your internal customer ID
});
// Create endpoint for this customer
const endpoint = await svix.endpoint.create(app.id, {
url: "https://acme.corp/webhooks/your-product",
version: 1,
description: "Production webhook endpoint",
filterTypes: ["invoice.paid", "subscription.cancelled"],
});
// Send a message — Svix handles delivery, retries, and logging
const message = await svix.message.create(app.id, {
eventType: "invoice.paid",
payload: {
invoiceId: "inv_123",
amount: 9900,
currency: "usd",
customerId: "cust_456",
},
});
console.log(`Message ${message.id} queued for delivery`);
Message Filtering and Fan-Out
// Customers can subscribe to specific event types
// Your code filters at send time — only matching endpoints receive the event
const message = await svix.message.create(app.id, {
eventType: "user.created",
payload: { userId: "user_789", email: "new@example.com", plan: "pro" },
// Optional: target specific channels
channels: ["pro-tier-events"],
});
// Query delivery attempts for debugging
const attempts = await svix.messageAttempt.listByMsg(app.id, message.id, {
status: 0, // 0=success, 1=pending, 2=fail
limit: 20,
});
for (const attempt of attempts.data) {
console.log(
`Endpoint: ${attempt.endpointId} | Status: ${attempt.status} | ` +
`HTTP: ${attempt.responseStatusCode} | Duration: ${attempt.timestamp}`
);
}
Embeddable Customer Portal
// Generate a magic link for your customer's webhook management portal
import { Svix } from "svix";
import express from "express";
const app = express();
const svix = new Svix(process.env.SVIX_AUTH_TOKEN!);
app.get("/webhook-portal", async (req, res) => {
const { customerId } = req.user; // your auth middleware
const portalAccess = await svix.authentication.appPortalAccess(
customerId, // application UID
{
featureFlags: [
"filteringEnabled",
"messageHistoryEnabled",
],
}
);
// Redirect customer to their self-serve portal
// They can add/remove endpoints, view history, replay messages
res.redirect(portalAccess.url);
});
Event Catalog with Schema Validation
// Define your event types with OpenAPI-style schemas
const eventType = await svix.eventType.create({
name: "invoice.paid",
description: "Fires when an invoice is successfully paid",
schemas: {
"1": {
description: "Invoice paid event v1",
properties: {
invoiceId: { type: "string", description: "Unique invoice identifier" },
amount: { type: "integer", description: "Amount in smallest currency unit" },
currency: { type: "string", enum: ["usd", "eur", "gbp"] },
},
required: ["invoiceId", "amount", "currency"],
},
},
});
Webhook Signature Verification
import { Webhook } from "svix";
// In your customer's receiving endpoint (or for testing)
const wh = new Webhook(process.env.SVIX_WEBHOOK_SECRET!);
app.post("/svix-webhooks", express.raw({ type: "application/json" }), (req, res) => {
const payload = req.body.toString();
const headers = {
"svix-id": req.headers["svix-id"] as string,
"svix-timestamp": req.headers["svix-timestamp"] as string,
"svix-signature": req.headers["svix-signature"] as string,
};
try {
const msg = wh.verify(payload, headers);
// Process verified webhook
console.log(`Verified event: ${msg.type}`, msg.data);
res.status(200).json({ received: true });
} catch (err) {
console.error("Webhook verification failed:", err.message);
res.status(400).json({ error: "Invalid signature" });
}
});
Hookdeck: Built for Inbound Webhook Reliability
Hookdeck flips the model — instead of sending webhooks, it's a managed proxy and router that sits in front of your receiving endpoints. Connect it to Stripe, GitHub, Shopify, or any third-party service, and Hookdeck handles buffering, retries, rate limiting, and transformations.
CLI Setup and Local Development
# Install Hookdeck CLI
npm install -g hookdeck-cli
# Login and create a tunnel for local development
hookdeck login
hookdeck listen 3000 stripe-webhooks
# → Creates a public URL you paste into Stripe's dashboard
# → All events are buffered and forwarded to localhost:3000
Node.js SDK Integration
import { HookdeckClient } from "@hookdeck/sdk";
const hookdeck = new HookdeckClient({
token: process.env.HOOKDECK_API_KEY!,
});
// Create a source (represents an inbound connection)
const source = await hookdeck.source.upsert({
name: "stripe-production",
});
// Create a destination (your endpoint)
const destination = await hookdeck.destination.upsert({
name: "order-service",
url: "https://api.yourapp.com/webhooks/stripe",
});
// Create a connection with rules
const connection = await hookdeck.connection.upsert({
name: "stripe-to-orders",
source: { id: source.id },
destination: { id: destination.id },
rules: [
{
type: "retry",
strategy: "exponential_backoff",
count: 5,
interval: 10000, // 10 seconds initial
responseStatusCodesToRetry: [500, 502, 503, 504, 429],
},
{
type: "alert",
strategy: "last_attempt",
channel: {
type: "slack",
name: "webhook-alerts",
},
},
],
});
console.log(`Source URL: ${source.url}`);
// → Set this in Stripe dashboard as your webhook endpoint
Filtering and Fan-Out
// Route different event types to different services
const checkoutConnection = await hookdeck.connection.upsert({
name: "stripe-to-checkout",
source: { id: source.id },
destination: { id: checkoutDestination.id },
rules: [
{
type: "filter",
body: {
type: {
$match: "checkout.session.completed",
},
},
},
],
});
const subscriptionConnection = await hookdeck.connection.upsert({
name: "stripe-to-subscriptions",
source: { id: source.id },
destination: { id: subscriptionDestination.id },
rules: [
{
type: "filter",
body: {
type: {
$match: "customer.subscription.*",
},
},
},
],
});
// Both connections share the same Stripe source URL
// Fan-out is handled by Hookdeck automatically
Transformations
// Transform incoming payloads before forwarding
const connection = await hookdeck.connection.upsert({
name: "github-to-cicd",
source: { id: githubSource.id },
destination: { id: cicdDestination.id },
rules: [
{
type: "transform",
transformation: {
name: "normalize-github-push",
code: `
addLayer({
normalized: {
repo: payload.repository.full_name,
branch: payload.ref.replace('refs/heads/', ''),
commit: payload.head_commit.id,
author: payload.pusher.name,
}
});
`,
},
},
],
});
Replay Failed Events
// Programmatically replay failed deliveries
const failedEvents = await hookdeck.event.list({
status: "failed",
connectionId: connection.id,
limit: 100,
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours
},
});
console.log(`Found ${failedEvents.count} failed events`);
// Bulk retry
for (const event of failedEvents.models) {
await hookdeck.event.retry(event.id);
}
Convoy: Open Source Webhook Gateway
Convoy is a fully open-source webhook gateway (Apache 2.0) that you self-host on your own infrastructure. It handles both inbound and outbound webhooks, gives you full data ownership, and ships as a single Docker container.
Docker Compose Setup
# docker-compose.yml
version: "3.8"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: convoy
POSTGRES_USER: convoy
POSTGRES_PASSWORD: convoy_secret
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
convoy:
image: getconvoy/convoy:latest
ports:
- "5005:5005"
environment:
CONVOY_DB_DRIVER: postgres
CONVOY_DB_DSN: "postgres://convoy:convoy_secret@postgres:5432/convoy"
CONVOY_REDIS_DSN: "redis://redis:6379"
CONVOY_JWT_SECRET: "your-32-char-secret-here"
CONVOY_API_KEY_HASH: "sha256"
depends_on:
- postgres
- redis
volumes:
postgres_data:
Go SDK Usage (Primary SDK)
package main
import (
"context"
"github.com/frain-dev/convoy-go/v2"
"os"
)
func main() {
c := convoy.New(
os.Getenv("CONVOY_BASE_URL"), // e.g., https://convoy.yourcompany.com
os.Getenv("CONVOY_API_KEY"),
os.Getenv("CONVOY_PROJECT_ID"),
)
ctx := context.Background()
// Create an endpoint (equivalent to Svix application/endpoint)
endpoint, err := c.Endpoints.Create(ctx, &convoy.CreateEndpointRequest{
Name: "Customer A Webhook",
URL: "https://customer-a.com/webhooks",
Description: "Production endpoint",
Secret: "custom-signing-secret",
RateLimit: 100,
RateLimitDuration: "1m",
})
if err != nil {
panic(err)
}
// Send an event
event, err := c.EventService.CreateEvent(ctx, &convoy.CreateEventRequest{
EndpointID: endpoint.UID,
EventType: "payment.completed",
Data: []byte(`{
"paymentId": "pay_123",
"amount": 5000,
"currency": "usd"
}`),
IdempotencyKey: "pay_123-completed-2026-03-09",
})
if err != nil {
panic(err)
}
println("Event created:", event.UID)
}
Node.js Client (HTTP API)
import axios from "axios";
const convoy = axios.create({
baseURL: process.env.CONVOY_BASE_URL + "/api/v1",
headers: {
Authorization: `Bearer ${process.env.CONVOY_API_KEY}`,
"Content-Type": "application/json",
},
});
// Create a fan-out event (sends to all matching endpoints in a project)
const response = await convoy.post(
`/projects/${process.env.CONVOY_PROJECT_ID}/events/fanout`,
{
owner_id: "customer-tenant-123",
event_type: "order.shipped",
data: {
orderId: "ord_789",
trackingNumber: "1Z999AA1234567890",
estimatedDelivery: "2026-03-12",
},
idempotency_key: `ord_789-shipped-${Date.now()}`,
}
);
console.log("Fan-out event:", response.data.data.uid);
Convoy CLI Deployment
# Install Convoy CLI
curl -L https://github.com/frain-dev/convoy/releases/latest/download/convoy-linux-amd64 -o convoy
chmod +x convoy && sudo mv convoy /usr/local/bin/
# Start server
convoy server \
--db-dsn "postgres://convoy:secret@localhost:5432/convoy" \
--redis-dsn "redis://localhost:6379" \
--port 5005 \
--log-level info
# Create organization and project via CLI
convoy project create --name "Production" --type outgoing
Feature Comparison
| Feature | Svix | Hookdeck | Convoy |
|---|---|---|---|
| Primary use case | Outbound (sending to customers) | Inbound (receiving from 3rd parties) | Both inbound + outbound |
| Open source | ❌ | ❌ | ✅ Apache 2.0 |
| Self-hosted | Enterprise only | ❌ | ✅ |
| Embeddable customer portal | ✅ First-class | ❌ | ✅ Basic |
| Multi-tenant support | ✅ Native | ❌ | ✅ |
| Webhook transformations | ❌ | ✅ | ❌ |
| Fan-out / routing | ✅ (filter types) | ✅ (connections) | ✅ (fan-out events) |
| Event replay | ✅ | ✅ | ✅ |
| Dead letter queue | ✅ | ✅ | ✅ |
| SDK languages | 12+ | 6 | Go (official), REST |
| Local dev tunnel | ✅ (svix listen) | ✅ (hookdeck listen) | ❌ |
| Idempotency keys | ✅ | ✅ | ✅ |
| Rate limiting | ✅ (per endpoint) | ✅ (per connection) | ✅ (per endpoint) |
| Signature verification | ✅ | ✅ | ✅ |
| Event catalog / schemas | ✅ | ❌ | ❌ |
| Alerting / notifications | ✅ | ✅ Slack/PagerDuty | ✅ Slack/email |
| Free tier | 25k events/month | 25k events/month | Unlimited (self-hosted) |
When to Use Each
Choose Svix if:
- You're building a B2B SaaS product that sends webhooks to your customers
- You need an embeddable portal so customers can manage their own endpoints
- You want schema-validated event types and a proper event catalog
- TypeScript/Node.js SDK quality is a priority
Choose Hookdeck if:
- You're the one receiving webhooks from third-party services (Stripe, GitHub, Shopify)
- You want to fan-out a single Stripe webhook to multiple internal services
- You need payload transformations before events reach your app
- Local development tunneling with replay is important
Choose Convoy if:
- Data sovereignty or compliance requires on-premises deployment
- You need both inbound and outbound webhook handling in one self-hosted system
- Cost predictability matters more than managed convenience
- Your team is comfortable operating Go/Redis/Postgres infrastructure
Avoid webhook infrastructure platforms entirely if:
- Your webhook volume is under 1,000 events/month — a simple BullMQ queue with retry logic handles this fine
- You control both the sender and receiver and have no customers to expose endpoints to
Reliability Deep Dive
All three implement the same core reliability pattern: exponential backoff with jitter, configurable retry counts (usually 5–10 attempts over 3 days), and dead-letter queues for events that exhaust all retries.
Where they differ:
Svix adds delivery rate analytics per endpoint and per event type. You can detect when a customer's endpoint is consistently failing and proactively notify them.
Hookdeck adds a 10-second response window check — if your endpoint doesn't respond within 10 seconds, Hookdeck treats it as a timeout and queues for retry. This prevents slow endpoints from blocking the delivery queue.
Convoy requires you to configure your own monitoring and alerting. The built-in dashboard shows delivery status but doesn't send proactive alerts by default.
Pricing Comparison (2026)
| Tier | Svix | Hookdeck | Convoy |
|---|---|---|---|
| Free | 25k events/mo | 25k events/mo | Unlimited (self-hosted) |
| Developer | ~$75/mo (250k events) | ~$100/mo (500k events) | Cloud: ~$99/mo |
| Pro | ~$350/mo (2M events) | ~$350/mo (5M events) | Cloud: ~$299/mo |
| Self-hosted | Enterprise only | ❌ | ✅ Free |
| Pricing model | Event volume | Event volume | Event volume / seats |
Pricing as of Q1 2026. Verify current plans at each vendor's website.
Methodology
Data sourced from official documentation, GitHub repositories, npm download trends, and community discussions on Discord and GitHub Issues. Star counts: Svix (3.4k), Hookdeck (1.7k), Convoy (4.2k) as of January 2026. Pricing based on published tiers; enterprise pricing requires contacting sales. Code examples verified against current SDK versions: Svix 1.x, Hookdeck SDK 0.5.x, Convoy Go SDK v2.
Need to compare webhook infrastructure against message queues? See BullMQ vs Inngest vs Trigger.dev for job queue alternatives, or explore Redpanda vs NATS vs Apache Kafka for high-throughput event streaming.