Webhooks
Webhooks are how WhatIsUp.dev tells you about things — incoming messages, instance state changes, delivery acks. You register one HTTPS URL per instance (or per customer), and we POST a signed JSON body whenever something happens.
The model
WhatsApp ──► Baileys ──► event_bus ──► BullMQ queue ──► worker ──► your endpoint
│
└─► webhook_deliveries (DB)
Every event is recorded in webhook_deliveries before we attempt to send. That row carries the full payload, the attempt count, and the last response status. It's queryable via the dashboard or the GET /v1/webhook-deliveries API.
Signature
Every outbound webhook carries an X-WhatIsUp-Signature header in the same shape Stripe uses:
X-WhatIsUp-Signature: t=1700000000,v1=<hex_hmac>
The HMAC is computed over <timestamp>.<raw_body> with your endpoint's signing secret. You verify it by recomputing and comparing in constant time. The full how-to lives at Webhooks → Signature verification.
Retries and backoff
Failed deliveries (anything that's not 2xx, including timeouts) are retried with exponential backoff + jitter:
| Attempt | Delay |
|---|---|
| 1 (immediate) | 0s |
| 2 | ~10s |
| 3 | ~60s |
| 4 | ~5m |
| 5 | ~30m |
| 6 | ~2h |
| Final | gives up after attempt 6 |
The exact delay has random jitter to avoid thundering-herd retries when you push a fix and a backlog drains. Total retry budget is ~2.5 hours.
Idempotency
Every event carries an event_id (ULID) that's stable across retries. Use it as your dedupe key:
async function handleWebhook(req) {
const eventId = req.body.event_id;
if (await alreadyProcessed(eventId)) return res.status(200);
// ... do the work ...
await markProcessed(eventId);
}Without dedupe, a slow ack from your side will cause the same event to be processed multiple times after retry.
Per-host concurrency
The worker caps how many requests can be in flight to any single host (default WEBHOOK_MAX_PER_HOST=4). If you have 50 instances all delivering to webhooks.example.com, only 4 are firing at once; the rest queue. This keeps a slow customer endpoint from saturating the worker on its own — your other endpoints keep flowing.
Payload retention
The webhook_deliveries.payload column is null'd by a sweeper after 7 days for successful deliveries and 30 days for failed/retrying ones. Metadata (status, attempt count, last response) sticks around for forensic queries. If you want longer payload retention, log it on your side.
Correlation IDs
Every webhook carries an X-WhatIsUp-Correlation-Id that lets you trace it back to whatever triggered it — usually a Fastify req.id from the API call that produced the event, or a generated ID for spontaneous events (incoming messages, state changes). Useful when you're trying to figure out which one of your test runs caused the spike in your logs.