Sending an OTP or a notification over WhatsApp is easy… until you look at the price of the official APIs and how painful their approval process is. For my projects (Mon Bara, TransitSoft), I needed an internal WhatsApp channel — lightweight and reusable. That's WhatsApp Internal API: a single building block that all my products consume through an SDK.
Baileys, no Chromium
A lot of solutions run WhatsApp Web inside a headless Chromium — heavy, fragile, and RAM-hungry. I went with Baileys, which speaks the WhatsApp protocol directly over WebSocket, no browser needed. The result: it runs on a tiny VPS without you ever thinking about it.
- Server: Express + Socket.IO + Baileys + BullMQ (port 4000)
- Dashboard: Next.js (create a session, scan the QR code, generate keys)
- packages/shared: types + Zod schemas + socket events — source of truth
- packages/sdk: the npm client
@wa/sdk
Multi-session, with per-session keys
Each session = one WhatsApp number. You create it from the dashboard, scan the
QR code with your phone, then generate an API key shown only once. A
consuming project never sees Baileys: it just has a baseUrl and a key.
Rate limiting is survival
WhatsApp is quick to ban a number that spams. So every send goes through a rate-limited BullMQ queue (15 msg/min, configurable), with automatic retries (3 attempts, backoff) and controlled concurrency. Statuses come back in real time over Socket.IO:
queued → sending → sent → delivered → read | failed
A failure? You replay it from the dashboard, or it re-enqueues itself.
One line on the consumer side
This is where the SDK pays off. In any Node ≥ 18 project:
import { WhatsAppClient } from "@wa/sdk";
const wa = new WhatsAppClient({ baseUrl, apiKey: process.env.WA_API_KEY! });
await wa.sendOtp("237655388662", "482913");
The SDK is published by CI (GitLab) on an sdk-vX.Y.Z tag, and the service
deploys behind Caddy (automatic TLS) — the compose file exposes nothing but
localhost; postgres and redis stay internal.
What I took away from it
The best decision was making it a single internal building block instead of bolting WhatsApp onto every project. A queue + a rate limit + a typed SDK, and all my products send OTPs without reinventing the wheel — or risking a ban.
How do you handle transactional messaging (SMS, WhatsApp, email) on your end — a homemade service or a third-party provider? And how do you stay within the rate limits? Let me know.