Baileys is how a great many WhatsApp integrations start. It is a TypeScript library that speaks the WhatsApp Web protocol directly over a WebSocket, it is MIT-licensed and free, and an evening with it gets you a bot that answers messages. Then the bot goes to production, and the evening turns into a job: keeping the socket alive, storing the session, surviving the next protocol change, and explaining to a customer why the number was banned. This post is for the team at that point. It is honest about what a hosted API takes away as well as what it gives, maps every Baileys concept you are using onto its Wapito equivalent, and lays out a migration that moves one number at a time without a big-bang cutover.
We run whatsmeow rather than Baileys under the default engine, and we compete with self-hosting, so read this knowing where it comes from. The facts about Baileys below are the ones on our alternatives page, each checked on the date shown there.
What a Baileys deployment actually costs
The library is free. What it does not include is everything around it, and in production that is most of the work.
An always-on process per number. Baileys holds a WebSocket to WhatsApp for as long as the
number is "online". Your process has to stay up, reconnect on every connection.update with a
DisconnectReason that permits it, and not reconnect on the ones that do not — the difference
between a hiccup and a logout, and between a logout and a ban, is a status code you have to
interpret correctly at 3 a.m.
Session state you own. useMultiFileAuthState writes the keys that make the linked device
your device. Lose them and the number needs re-pairing; leak them and someone else is your
device. In practice that means a database-backed auth state, backups, and a story for what
happens when two replicas start with the same keys. The next major version of the library is
expected to change the auth-state format, which turns a version bump into a migration for every
stored session.
Media. An inbound image is an encrypted blob and a key; downloadMediaMessage decrypts it,
and then it is your problem to store, serve and expire. Outbound media needs the reverse.
Pacing and ban risk. Nothing in the library slows you down. A for loop over a contact list
will send as fast as the socket allows, which is exactly the pattern WhatsApp bans. Every team
ends up writing its own delay, its own warm-up, its own "do not message people who never wrote
to us" rule — or does not, and loses the number.
Keeping up. As of September 2026 the current major version has sat on release candidates for around ten months, so teams choose between an old stable line and a moving target. The issue tracker carries several hundred open issues, which makes it hard to tell whether a problem is yours, a known bug, or a protocol change nobody has caught up with. Version seven stopped emitting delivery acknowledgements for incoming messages because WhatsApp was banning numbers that sent them too eagerly — a reminder that library behaviour and ban risk are the same subject.
Observability. Which messages failed, why, and what the socket was doing at the time are things you log, store and dashboard yourself.
None of this is a criticism of the library, which is remarkable engineering. It is a description of the job you take on when protocol access is your plumbing rather than your product.
What a hosted API takes away
Be clear-eyed about the other direction too.
- Protocol access. Baileys hands you every field WhatsApp sends. Wapito hands you a curated
contract: a stable message object, fourteen event types, a fixed set of endpoints. If you rely
on an obscure field, check the API reference before you commit;
include_rawon the channel puts the engine's raw payload beside every message, but the contract is the contract. - A dependency on someone else's uptime. You trade your on-call for ours.
- A bill. A Sandbox channel is free and limited; a Premium channel is a flat monthly price per number. Against an engineer-day a month of upkeep it is usually not close, but it is a line item where there was none.
- Ban risk does not go away. It changes who implements the controls. The warm-up ladder, the
cold-send guard and the pacing are on by default and cannot be switched off, which is safer
than a hurried
setTimeout, but a number sending unwanted messages is still a number that will be banned. The anti-ban guide is the honest version.
The concept map
Most Baileys code falls into a dozen calls. Here is each one and its equivalent, with the reference page where the full request and response live.
| Baileys | Wapito | Notes |
|---|---|---|
makeWASocket() + useMultiFileAuthState() | A channel in the dashboard | No session files. The channel is the linked device; Wapito keeps its state. |
sock.requestPairingCode(phone) / the qr field of connection.update | Connect tab, or POST /channel/pairing-code | Pairing code first, QR one link away. |
sock.ev.on('connection.update') | The channel webhook event | status and reason (banned, logged_out, user_logout, engine_failed) instead of DisconnectReason arithmetic. |
sock.ev.on('messages.upsert') | A webhook subscribed to messages | POST /webhooks with events: ["messages"]; signed deliveries, retried on failure. |
sock.ev.on('messages.update') (acks) | messages.status | failed, pending, sent, delivered, read, played |
sock.sendMessage(jid, { text }) | POST /messages/text | Send without a template. quoted, mentions, typing_time are body fields. |
sock.sendMessage(jid, { image: { url }, caption }) | POST /messages/image | media takes a URL, a data URI or an uploaded med_ id. |
downloadMediaMessage(msg) | The link on every media message | Already decrypted, stored, signed; 24 h on Sandbox, 7 days on Premium. |
sock.sendPresenceUpdate('composing', jid) | typing_time on the send, or POST /presence/{chat_id} | Automatic with typing_simulation: "auto". |
sock.readMessages([key]) | PUT /messages/{id}/read, or auto_read in settings | |
sock.onWhatsApp(number) | POST /contacts/check | Up to fifty numbers per call, paced, quota-counted. |
sock.groupCreate(subject, participants) | POST /groups | Create a group. |
sock.groupParticipantsUpdate(jid, ids, 'add') | POST /groups/{id}/participants | Add participants; invite_required per person instead of a silent skip. |
sock.groupInviteCode(jid) | GET /groups/{id}/invite | Returns the code and the full link. |
sock.groupMetadata(jid) | GET /groups/{id} | |
sock.profilePictureUrl(jid) | GET /contacts/{id}/profile | |
jidNormalizedUser(), @lid handling | Done for you | Ids come back as @s.whatsapp.net; LIDs resolved where possible, from_lid otherwise. |
Three shape differences to plan for:
- Message ids. Baileys gives you
key.idandkey.remoteJidseparately. Wapito'sidis one string of the form<fromMe>_<chat>_<id>— treat it as opaque and store it whole. - Timestamps. Baileys reports
messageTimestampin seconds; Wapito'stimestampis milliseconds. - Events are HTTP, not callbacks. Your
messages.upserthandler becomes an HTTPS endpoint that verifies a signature and answers200fast. The webhooks guide has the verifier in Node, Python and PHP; delivery is at-least-once and unordered, so key on the envelopeid.
Side by side: send and receive
The most common Baileys program, and the same thing against Wapito.
import makeWASocket, { useMultiFileAuthState, DisconnectReason } from '@whiskeysockets/baileys';
const { state, saveCreds } = await useMultiFileAuthState('./auth');
const sock = makeWASocket({ auth: state });
sock.ev.on('creds.update', saveCreds);
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode;
if (code !== DisconnectReason.loggedOut) start(); // your reconnect logic
}
});
sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
if (msg.key.fromMe || !msg.message?.conversation) continue;
await sock.sendMessage(msg.key.remoteJid, { text: 'Thanks, we will reply shortly.' });
}
});
The Wapito side has no socket, no auth folder and no reconnect branch, and the reply is paced,
typed and rate-limited by the platform rather than by whatever setTimeout the Baileys version
forgot. What it has instead is a signature to verify and an HTTP status to return quickly.
What has to be re-paired, and what survives
A WhatsApp session is bound to the library and the device registration that created it. The
auth folder Baileys wrote cannot be imported into Wapito or into anything else; the number has
to be linked again, as a new device. Plan it as a short maintenance window per number:
- Survives: the number, the account, its contacts, every chat and group on the phone, the group memberships and admin roles, the profile. None of that lives in the linked device.
- Replaced: the linked device. Log the Baileys device out first — from the phone's Linked devices screen, or by letting Baileys receive the logout — before linking Wapito. A number can have several linked devices, and leaving a dead one registered is both a security smell and one more thing WhatsApp sees.
- Not migrated: message history. Wapito stores what happens from the moment the channel is linked; Baileys' history sync is not replayed. Keep your database, and stop assuming the provider is your archive — it never was.
- Reset: the warm-up ladder. A newly linked number starts at 50 messages on its first day and climbs over a week. For a number that was already sending hundreds a day this is the part that needs scheduling: link it a week before it takes production traffic, or migrate it during a quiet period.
Moving one number at a time
A Baileys deployment with several numbers should not be cut over in one evening. The shape that works:
1. Inventory. For each number: the process that runs it, the handlers it registers, the
calls it makes (grep for sock.), the daily volume, and whether it ever messages people who
did not write first. That last column decides how much the cold-send guard will change its
behaviour.
2. Build the receiver once. One webhook endpoint that verifies signatures, writes the
envelope to a queue and returns 200. Route on X-Wapito-Channel so a single endpoint serves
every number. Test it with POST /webhooks/{id}/test against a Sandbox channel before any
production number moves.
3. Port the handlers behind a flag. Wrap each Baileys handler's body in a function that takes your own normalised message shape, and write a second adapter from the Wapito event to that shape. The business logic does not know which side is feeding it.
4. Move the quietest number first. Log the Baileys device out, link the number to a
channel, flip the flag, watch the logs for a day.
Expect 429s from the warm-up ladder and the cold-send guard if the number was busy; they are
the platform doing what your setTimeout used to, and the rate limits page
explains each code.
5. Repeat, one number a day. Each number gets its own token; nothing in the code changes between them except an environment variable.
6. Turn the Baileys process off last, once no number points at it, and delete the auth folders — they are credentials, and they are now credentials to devices that no longer exist.
Never run the same number through both at once. Two linked devices driven by two automations is the exact pattern an account-takeover check flags, and it is the one migration mistake that costs a number.
Is it the right move?
Keep Baileys if protocol access is your product: you are building a client, you need a field the contract does not carry, or your volume is one number and you enjoy the upkeep. Move if the socket, the session files, the reconnect logic and the ban-risk controls are things you would rather not own, and you are prepared to pay a flat price per number to stop owning them. Most teams that ask the question are already past the point where the answer is in doubt; what they need is the map above and a quiet week to walk it.
The Baileys alternatives page has the pricing comparison with sources and dates, and the whatsmeow page explains why the default engine underneath a Wapito channel is the other library.