Webhook management
Reliable webhook delivery with retries, signature verification, circuit breaker, and DLQ
Webhooks are the most common way to deliver events to external systems. npayload turns unreliable HTTP calls into guaranteed delivery with automatic retries, circuit breakers, and a dead letter queue for failed deliveries.
The challenge
Building reliable webhook delivery is harder than it looks:
- Retries: Endpoints go down. You need exponential backoff without overwhelming the target.
- Signatures: Receivers need to verify that webhooks are authentic.
- Circuit breaking: A failing endpoint should not consume all your retry budget.
- Observability: You need to know which webhooks failed and why.
- Replay: When an endpoint recovers, you need to replay missed webhooks.
How npayload solves it
Publish a message to a channel. npayload delivers it to every webhook subscriber with retries, signatures, and circuit breaking built in.
Set up webhook delivery
// Create a channel for your events
await npayload.channels.create({
name: 'platform-events',
description: 'Events from the platform API',
});
// Register a webhook subscriber
await npayload.subscriptions.create({
channel: 'platform-events',
name: 'customer-webhook',
type: 'webhook',
endpoint: {
url: 'https://customer.example.com/webhooks/events',
method: 'POST',
headers: {
'X-Webhook-Source': 'my-platform',
},
},
delivery: {
timeoutMs: 30000,
retryPolicy: {
maxAttempts: 6,
initialDelayMs: 1000,
maxDelayMs: 7200000, // 2 hours
backoffMultiplier: 2,
},
},
});Publish events
await npayload.messages.publish({
channel: 'platform-events',
routingKey: 'invoice.paid',
payload: {
event: 'invoice.paid',
invoiceId: 'inv_001',
amount: 299.00,
currency: 'USD',
paidAt: new Date().toISOString(),
},
});npayload delivers this to every webhook subscriber. Each delivery includes:
- Signature header (
x-npayload-signature) for verification - Retry on failure with exponential backoff
- Unique delivery ID for deduplication
- Timestamp for replay ordering
Signature verification (receiver side)
Webhook receivers should verify the signature before processing:
// In your webhook endpoint
app.post('/webhooks/events', (req, res) => {
const isValid = npayload.webhooks.verify(
req.body,
req.headers['x-npayload-signature'],
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
handleEvent(req.body.payload);
res.status(200).json({ received: true });
});Retry behavior
npayload retries failed deliveries with exponential backoff:
| Attempt | Delay | Total elapsed |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 second | 1s |
| 3 | 30 seconds | 31s |
| 4 | 5 minutes | ~5.5 min |
| 5 | 30 minutes | ~35 min |
| 6 | 2 hours | ~2.5 hours |
A delivery is retried when:
- The endpoint returns a 5xx status code
- The request times out
- The connection is refused
A delivery is not retried when:
- The endpoint returns 2xx (success)
- The endpoint returns 4xx (client error, except 429)
- The endpoint returns 429 (rate limited, retried with the
Retry-Afterheader)
Circuit breaker
Every subscription has a built-in circuit breaker. If an endpoint fails repeatedly, the circuit opens and deliveries are paused to protect both sides.
| State | Behavior |
|---|---|
| Closed | Normal delivery |
| Open | Deliveries paused, messages queued |
| Half-open | Testing with a single delivery |
When the endpoint recovers, the circuit breaker closes automatically and queued messages are delivered.
// Manually reset a circuit breaker if needed
await npayload.subscriptions.resetCircuit('sub_abc123');Dead letter queue
After all retry attempts are exhausted, failed deliveries move to the DLQ. Nothing is lost.
// Inspect failed deliveries
const entries = await npayload.dlq.list({
subscriptionId: 'sub_abc123',
limit: 50,
});
for (const entry of entries.items) {
console.log(`Failed: ${entry.messageId}`);
console.log(`Reason: ${entry.failureReason}`);
console.log(`Attempts: ${entry.attemptCount}`);
}
// Replay a single entry
await npayload.dlq.replay(entry.gid);
// Replay all entries for a subscription
await npayload.dlq.replayAll({ subscriptionId: 'sub_abc123' });Multi-tenant webhook delivery
If you are building a platform that delivers webhooks to your customers, npayload scales to thousands of endpoints:
// Each customer gets their own subscription
for (const customer of customers) {
await npayload.subscriptions.create({
channel: 'platform-events',
name: `webhook-${customer.id}`,
type: 'webhook',
filter: { routingKey: `customer.${customer.id}.*` },
endpoint: {
url: customer.webhookUrl,
},
});
}Each customer's webhook has independent retries, circuit breaking, and DLQ.
Why npayload for webhooks
| Feature | Benefit |
|---|---|
| Automatic retries | Configurable backoff, up to days of retry |
| Circuit breaker | Protect failing endpoints automatically |
| Signature verification | HMAC signatures on every delivery |
| Dead letter queue | Failed deliveries never lost, always replayable |
| Per-subscriber isolation | One customer's failures do not affect others |
| Delivery tracking | Full observability into every delivery attempt |
| Routing filters | Deliver only relevant events per subscriber |
Next steps
- Webhooks guide for detailed webhook configuration
- Dead letter queue for DLQ management
- Subscriptions for delivery options
Was this page helpful?