Baileys WhatsApp API in Production: Webhooks, Sessions & Anti-Ban
Running a Baileys-based WhatsApp API in production means solving session persistence, signed webhooks, and anti-ban pacing. Here's how WaSphere does it.
Baileys WhatsApp API in Production: Webhooks, Sessions & Anti-Ban
Baileys is the most capable open-source library for talking to WhatsApp's multi-device protocol over WebSocket. It's also a library, not a product — the moment you put it in production you inherit a pile of operational problems Baileys doesn't solve for you: keeping sessions alive across restarts, delivering inbound events reliably, and pacing your sends so WhatsApp doesn't flag the number.
WaSphere wraps Baileys into a self-hosted platform and handles exactly those concerns. This post walks through the three that matter most: sessions, webhooks, and anti-ban.
Sessions: surviving restarts and running many at once
Baileys authenticates by writing credential and signal-key state to disk. If you lose that state, the session is gone and the user has to re-scan a QR code. Two things break naive setups:
- Ephemeral storage. Run Baileys in a container without a persistent volume and every redeploy logs you out.
- One process, many numbers. Production means multiple WhatsApp numbers. Each is a separate Baileys socket with its own auth state, lifecycle, and reconnect logic.
WaSphere isolates all Baileys code in a single WA Server process and persists each session's credentials to a mounted volume, so containers can restart without re-pairing. Each session is an independent connection with its own status (connecting, connected, disconnected) and its own QR lifecycle. The dashboard streams the QR for pairing; once linked, the credentials persist.
Architecturally this isolation matters for another reason: Baileys depends on libsignal (GPLv3). WaSphere keeps Baileys confined to the MIT-licensed WA Server binary and never lets other code import it directly — everything else talks to it over HTTP. If you're building your own stack, draw the same boundary early.
Webhooks: delivering inbound events you can trust
A WhatsApp API that can only send is half a product. You need inbound messages, delivery receipts, read receipts, and session-status changes pushed to your application. Baileys emits these as in-process events — turning them into reliable, verifiable HTTP deliveries is on you.
WaSphere POSTs an event to your endpoint for each of 10 event types — including message.received, message.sent, message.failed, message.delivered, message.read, and session.disconnected. Every delivery carries signing headers:
Content-Type: application/json
X-WaSphere-Event: message.received
X-WaSphere-Signature: v1,sha256=<hmac-hex>
X-WaSphere-Timestamp: 1748168400
X-WaSphere-Delivery-Id: abc123
The signature is HMAC-SHA256(signingSecret, "{timestamp}.{rawBody}"), formatted as v1,sha256={hex}. Verify it before processing — without verification, anyone who learns your webhook URL can forge events. Verification in Node:
import crypto from 'crypto';
function verify(rawBody, signature, timestamp, secret) {
// Reject anything older than 5 minutes — replay-attack guard
if (Math.abs(Date.now() / 1000 - parseInt(timestamp, 10)) > 300) return false;
const expected = 'v1,sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Use the raw request body, not a re-serialized object — JSON re-encoding changes byte order and the HMAC won't match. Respond 2xx fast (under 10 seconds) and process asynchronously; if you block, the delivery is marked failed and retried, and you'll process duplicates. Failed deliveries retry with backoff (1s, 5s, 30s) before being marked failed in the delivery log.
Anti-ban: pacing so the number survives
WhatsApp watches for automation that doesn't behave like a human. Blasting hundreds of identical messages in a tight loop from a fresh number is the fastest way to a ban. Baileys will happily let you do exactly that — it has no opinion on pacing.
WaSphere applies platform-wide anti-ban defaults you can tune per session:
- Randomized inter-message delay between a configurable min and max, so sends aren't metronomic.
- Typing simulation — a typing indicator before text messages, scaled to message length.
- Per-session rate limiting — messages over the limit are queued, not dropped.
Practical guidance that no library gives you:
- Warm up new numbers. Start with low volume and ramp over days, not minutes.
- Use real, opted-in recipients. Sending to numbers that block or report you is the strongest ban signal.
- Dedicate the number to automation. Never run automation on a personal account.
- Vary content. Identical message bodies at high volume look like spam.
These are heuristics, not guarantees — WhatsApp's enforcement is opaque. But pacing, warming, and consent move the odds heavily in your favor.
Sending without touching Baileys directly
Once the platform owns the hard parts, sending is a plain HTTP call. The session is in the path; your scoped API key authenticates:
curl -X POST \
"https://api.your-domain.com/workspaces/{workspaceId}/proxy/api/sessions/{sessionId}/messages/text" \
-H "Authorization: Bearer wsk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{ "to": "447700900123", "text": "Hello from production." }'
Fourteen message types are supported — text, image, video, audio, document, sticker, location, contact, poll, reaction, list, buttons, template, and link preview — all behind the same endpoint shape.
The takeaway
Baileys gives you the protocol; production gives you the problems. Persistent multi-session state, signed and retried webhooks, and anti-ban pacing are the difference between a demo and something you can put a business on. WaSphere packages all of it as a self-hosted, MIT-licensed Docker stack so you don't rebuild it yourself.
Get a session connected in a few minutes with the Quick Start guide.