Webhooks
Reliable message delivery with retries, circuit breaker, and dead letter queue
npayload delivers messages to your endpoints via webhooks with automatic retries, exponential backoff, and a dead letter queue for failed deliveries.
How webhook delivery works
When you publish a message to a channel with webhook subscriptions:
- Fan-out. npayload routes the message to all matching subscriptions.
- Delivery attempt. HTTP POST to your endpoint with the message payload.
- Success. Your endpoint responds with
2xx. Delivery is marked successful. - Failure. Non-2xx response or timeout triggers the retry schedule.
Retry schedule
| Attempt | Delay | Cumulative |
|---|---|---|
| 1 (initial) | Immediate | 0s |
| 2 | 1 second | 1s |
| 3 | 30 seconds | 31s |
| 4 | 5 minutes | ~5.5m |
| 5 | 30 minutes | ~35.5m |
| 6 | 2 hours | ~2.5h |
After all retries are exhausted, the delivery is moved to the dead letter queue.
Signature verification
Every webhook request includes an HMAC signature in the x-npayload-signature header. Always verify this signature before processing the payload.
import express from 'express';
import { NPayloadClient } from '@npayload/node';
const app = express();
app.use(express.json());
app.post('/webhooks/orders', (req, res) => {
const signature = req.headers['x-npayload-signature'] as string;
const isValid = npayload.webhooks.verify(
req.body,
signature,
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the message
const { payload, metadata } = req.body;
console.log('Event:', payload.event);
// Always respond 200 to acknowledge receipt
res.status(200).json({ received: true });
});Webhook payload format
{
"messageGid": "msg_abc123",
"channelName": "orders",
"payload": {
"event": "order.created",
"orderId": "ord_12345",
"total": 59.98
},
"metadata": {
"publishedAt": "2026-03-06T12:00:00Z",
"attempt": 1
}
}Circuit breaker
If your endpoint fails consistently, npayload activates a circuit breaker:
- Closed (normal): deliveries proceed normally.
- Open (tripped): after consecutive failures, deliveries are paused and messages queue up.
- Half-open (testing): after a cooldown period, npayload sends a test delivery. If it succeeds, the circuit closes.
Only 5xx responses count as failures. 4xx responses are treated as permanent rejections and the message goes to the DLQ immediately.
Dead letter queue
Failed deliveries land in the DLQ. You can inspect, debug, and replay them:
// List DLQ entries
const entries = await npayload.dlq.list();
// Replay a single entry
await npayload.dlq.replay(entries[0].gid);
// Purge all entries
await npayload.dlq.purge();See the DLQ API reference for all operations.
Best practices
- Respond quickly. Return
200immediately, then process asynchronously. - Be idempotent. The same message may be delivered more than once.
- Verify signatures. Always check
x-npayload-signaturebefore processing. - Use HTTPS. All webhook endpoints must use HTTPS.
- Monitor the DLQ. Set up alerts for DLQ entries to catch integration issues early.
Next steps
- Node.js SDK for webhook verification helpers
- DLQ API to manage failed deliveries
- Subscriptions API to create webhook subscriptions
Was this page helpful?