Skip to main content
npayload is launching soon.
npayloadDocs
Guides

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:

  1. Fan-out. npayload routes the message to all matching subscriptions.
  2. Delivery attempt. HTTP POST to your endpoint with the message payload.
  3. Success. Your endpoint responds with 2xx. Delivery is marked successful.
  4. Failure. Non-2xx response or timeout triggers the retry schedule.

Retry schedule

AttemptDelayCumulative
1 (initial)Immediate0s
21 second1s
330 seconds31s
45 minutes~5.5m
530 minutes~35.5m
62 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 200 immediately, then process asynchronously.
  • Be idempotent. The same message may be delivered more than once.
  • Verify signatures. Always check x-npayload-signature before 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

Was this page helpful?

On this page