A lightweight WhatsApp API server using Baileys - a drop-in replacement for wwebjs-api with ~500MB less RAM usage per session.
- WebSocket-based connection (no browser/Puppeteer required)
- Full API parity with wwebjs-api (~70+ endpoints)
- QR code and pairing code authentication
- Webhook delivery for events
- WebSocket event stream at
/ws(session-filterable) - Legacy compatibility router at
/legacy/*for wwebjs endpoint migration - Multi-session support
- Auto-reconnection with exponential backoff
- OpenAPI + Swagger docs at
/openapi.jsonand/docs
cd baileys-api-server
bun installCopy .env.example to .env and configure:
PORT=3000
API_KEY=your_api_key_here
ENABLE_LEGACY_ROUTER=true
ENABLE_WEBSOCKET=true
ENABLE_WEBHOOK=true
BASE_WEBHOOK_URL=http://localhost:3001/api/ai-agents/webhook
SESSIONS_PATH=./sessions
AUTO_START_SESSIONS=true
LOG_LEVEL=infoDevelopment with hot-reload:
bun run devProduction:
bun run startInteractive docs:
GET /docs- Swagger UIGET /openapi.json- OpenAPI schema
GET /ping- Health checkGET /ws- WebSocket upgrade endpoint for realtime events (when enabled)
GET /session/start/:sessionId- Start sessionGET /session/stop/:sessionId- Stop session (keep auth)GET /session/status/:sessionId- Get session statusDELETE /session/terminate/:sessionId- Terminate session and delete authGET /session/getSessions- List in-memory sessionsGET /session/qr/:sessionId- Get QR textGET /session/qr/:sessionId/image- Get QR PNG imagePOST /session/requestPairingCode/:sessionId- Request phone pairing codeGET /session/logout/:sessionId- Logout sessionGET /session/restart/:sessionId- Restart session
POST /client/sendMessage/:sessionId- Send messagePOST /client/getChats/:sessionId- Get chatsPOST /client/getChatById/:sessionId- Get chat by idPOST /client/getContacts/:sessionId- Get contactsPOST /client/getContactById/:sessionId- Get contact by idPOST /client/isRegisteredUser/:sessionId- Check if number is on WhatsAppPOST /client/getInfo/:sessionId- Get client infoPOST /client/getLabels/:sessionId- Get labelsPOST /client/createGroup/:sessionId- Create groupPOST /client/getProfilePicUrl/:sessionId- Get profile picture URLPOST /client/getBlockedContacts/:sessionId- Get blocked contactsPOST /client/blockContact/:sessionId- Block contactPOST /client/unblockContact/:sessionId- Unblock contactPOST /client/setStatus/:sessionId- Set profile statusPOST /client/setDisplayName/:sessionId- Set display namePOST /client/getCommonGroups/:sessionId- Get common groups with contactPOST /client/getNumberId/:sessionId- Resolve number id/JIDPOST /client/sendPresenceUpdate/:sessionId- Send account presence state
POST /chat/fetchMessages/:sessionId- Fetch stored messagesPOST /chat/sendStateTyping/:sessionId- Send typing statePOST /chat/clearState/:sessionId- Clear typing/recording statePOST /chat/sendStateRecording/:sessionId- Send recording statePOST /chat/sendSeen/:sessionId- Mark chat seenPOST /chat/markUnread/:sessionId- Mark chat unreadPOST /chat/archive/:sessionId- Archive chatPOST /chat/unarchive/:sessionId- Unarchive chatPOST /chat/pin/:sessionId- Pin chatPOST /chat/unpin/:sessionId- Unpin chatPOST /chat/mute/:sessionId- Mute chatPOST /chat/unmute/:sessionId- Unmute chatPOST /chat/clearMessages/:sessionId- Clear local chat messagesPOST /chat/delete/:sessionId- Delete chatPOST /chat/getLabels/:sessionId- Get chat labelsPOST /chat/getContact/:sessionId- Get chat contact
POST /contact/getAbout/:sessionId- Get contact about/statusPOST /contact/getProfilePicUrl/:sessionId- Get contact profile picture URLPOST /contact/block/:sessionId- Block contactPOST /contact/unblock/:sessionId- Unblock contactPOST /contact/isBlocked/:sessionId- Check if contact is blockedPOST /contact/getCommonGroups/:sessionId- Get common groupsPOST /contact/getFormattedNumber/:sessionId- Get formatted numberPOST /contact/getCountryCode/:sessionId- Get country code
POST /groupChat/getGroupInfo/:sessionId- Get group metadataPOST /groupChat/getParticipants/:sessionId- Get participantsPOST /groupChat/addParticipants/:sessionId- Add participantsPOST /groupChat/removeParticipants/:sessionId- Remove participantsPOST /groupChat/promoteParticipants/:sessionId- Promote participantsPOST /groupChat/demoteParticipants/:sessionId- Demote participantsPOST /groupChat/setSubject/:sessionId- Set group subjectPOST /groupChat/setDescription/:sessionId- Set group descriptionPOST /groupChat/setMessagesAdminsOnly/:sessionId- Toggle admin-only messagingPOST /groupChat/setInfoAdminsOnly/:sessionId- Toggle admin-only info editingPOST /groupChat/leave/:sessionId- Leave groupPOST /groupChat/getInviteCode/:sessionId- Get invite codePOST /groupChat/revokeInvite/:sessionId- Revoke invite codePOST /groupChat/acceptInvite/:sessionId- Accept invite codePOST /groupChat/getInviteInfo/:sessionId- Get invite metadataPOST /groupChat/setPicture/:sessionId- Set group picturePOST /groupChat/deletePicture/:sessionId- Delete group picture
POST /message/getInfo/:sessionId- Get message receipts infoPOST /message/react/:sessionId- React to messagePOST /message/star/:sessionId- Star or unstar messagePOST /message/delete/:sessionId- Delete messagePOST /message/forward/:sessionId- Forward messagePOST /message/downloadMedia/:sessionId- Download media from messagePOST /message/getQuotedMessage/:sessionId- Get quoted messagePOST /message/getMentions/:sessionId- Get message mentionsPOST /message/edit/:sessionId- Edit messagePOST /message/pin/:sessionId- Pin messagePOST /message/unpin/:sessionId- Unpin message
- Set
ENABLE_LEGACY_ROUTER=trueto enable legacy compatibility endpoints. - Legacy endpoints are exposed under
/legacy/*. - This means you can call old wwebjs-style endpoints by changing base path to
/legacy. Example:- Old:
POST /client/sendMessage/:sessionId - Legacy compat:
POST /legacy/client/sendMessage/:sessionId
- Old:
Compatibility behavior:
- All endpoints from the old wwebjs Swagger are registered in the legacy router.
- Endpoints that can be bridged are forwarded to current implementations (including method/path/body adapters).
- Endpoints not implemented in the current Baileys server return
501 legacy_endpoint_not_implementedinstead of404. - Legacy routes are intentionally not included in
/openapi.jsonto keep primary docs focused on current API.
All sending uses:
POST /client/sendMessage/:sessionId- Header:
x-api-key: <API_KEY>(if configured) - Body required keys:
chatId,contentType,content
[email protected](wwebjs contact format)[email protected](Baileys contact format)1234567890(server auto-converts to WhatsApp user JID)[email protected](group JID)
Use the same endpoint for personal and group chats. Group sends require the session account to be a participant and allowed to send messages in that group.
string: plain text messageMessageMedia: base64 media payloadMessageMediaFromURL: media from public URLLocation: latitude/longitude location messagePoll: poll messageContact: send a contact cardButtons: fallback text rendering (interactive buttons are deprecated by WhatsApp)List: fallback text rendering (interactive lists are deprecated by WhatsApp)
options is optional and currently supports:
caption: media captionquotedMessageId: reply to an existing message idmentions: array of user IDs to mentionsendAudioAsVoice: send audio as voice note (ptt)sendVideoAsGif: send video as GIF playbacklinkPreview: enable/disable text link preview generation (default: enabled)
Not all options apply to all contentType values.
| Option | string | MessageMedia | MessageMediaFromURL | Location | Poll | Contact | Buttons | List |
|---|---|---|---|---|---|---|---|---|
quotedMessageId |
Yes | Yes | Yes | No | No | No | No | No |
mentions |
Yes | Yes | Yes | No | No | No | No | No |
caption |
No | Image/Video only | Image/Video only | No | No | No | No | No |
sendAudioAsVoice |
No | Audio only | Audio URL only | No | No | No | No | No |
sendVideoAsGif |
No | Video only | Video URL only | No | No | No | No | No |
linkPreview |
Yes | No | No | No | No | No | No | No |
Implementation details:
quotedMessageIdis best-effort. If message is not found in local store, send still succeeds without quote.linkPreviewonly applies to text messages and only the firsthttp/httpsURL in text is used.- Link preview generation has a
5stimeout and failures are non-blocking (message is still sent). MessageMediaFromURLfirst downloads media server-side (60stimeout), then sends it as normal media.- If
captionis not provided for image/video, server falls back tofilename, then empty string. - Audio/document messages ignore
captionin current implementation.
{
"chatId": "[email protected]",
"contentType": "string",
"content": "Hello from API"
}{
"chatId": "[email protected]",
"contentType": "string",
"content": "Hi @user, please review this: https://example.com/spec",
"options": {
"mentions": ["[email protected]"],
"quotedMessageId": "ABCD1234EFGH5678",
"linkPreview": false
}
}Note:
quotedMessageIdshould exist in local message store for that session/chat. If not found, message is still sent without quote context.mentionsaccepts IDs in@c.us,@s.whatsapp.net, or numeric form.
{
"chatId": "[email protected]",
"contentType": "MessageMedia",
"content": {
"mimetype": "image/jpeg",
"data": "<BASE64_IMAGE>",
"filename": "photo.jpg"
},
"options": {
"caption": "Image from base64"
}
}{
"chatId": "[email protected]",
"contentType": "MessageMedia",
"content": {
"mimetype": "video/mp4",
"data": "<BASE64_VIDEO>",
"filename": "clip.mp4"
},
"options": {
"caption": "Video clip",
"sendVideoAsGif": true
}
}{
"chatId": "[email protected]",
"contentType": "MessageMedia",
"content": {
"mimetype": "audio/ogg",
"data": "<BASE64_AUDIO>",
"filename": "note.ogg"
},
"options": {
"sendAudioAsVoice": true
}
}{
"chatId": "[email protected]",
"contentType": "MessageMedia",
"content": {
"mimetype": "application/pdf",
"data": "<BASE64_PDF>",
"filename": "invoice.pdf"
}
}{
"chatId": "[email protected]",
"contentType": "MessageMediaFromURL",
"content": "https://example.com/path/image.png",
"options": {
"caption": "Fetched from URL"
}
}{
"chatId": "[email protected]",
"contentType": "Location",
"content": {
"latitude": 37.7749,
"longitude": -122.4194,
"description": "San Francisco"
}
}{
"chatId": "[email protected]",
"contentType": "Poll",
"content": {
"pollName": "Deploy window?",
"pollOptions": ["Now", "Tonight", "Tomorrow"],
"options": {
"allowMultipleAnswers": false
}
}
}Poll behavior:
allowMultipleAnswers: falseallows one selection.allowMultipleAnswers: trueallows selecting up to all poll options.
{
"chatId": "[email protected]",
"contentType": "Contact",
"content": {
"contactId": "[email protected]"
}
}Contact behavior:
- Server builds a vCard using the numeric part of
contactId. contactIdaccepts numeric,@c.us, or@s.whatsapp.netformat.
These are accepted for compatibility, but WhatsApp interactive buttons/lists are deprecated in many clients. The server sends a text fallback.
Buttons example:
{
"chatId": "[email protected]",
"contentType": "Buttons",
"content": {
"title": "Actions",
"body": "Choose an option",
"footer": "Footer",
"buttons": [
{ "id": "one", "body": "Option 1" },
{ "id": "two", "body": "Option 2" }
]
}
}Compatibility behavior:
Buttons: fallback text usestitle,body,footer.buttons[]are not rendered as interactive actions.List: fallback text usestitle,body,footer.buttonTextandsections[]are not interactive in current implementation.- Account requirement: normal personal WhatsApp linked-device account works; no special business API account is required.
List example:
{
"chatId": "[email protected]",
"contentType": "List",
"content": {
"title": "Menu",
"body": "Pick one item",
"footer": "Footer",
"buttonText": "Open",
"sections": [
{
"title": "Main",
"rows": [
{ "id": "r1", "title": "Row 1", "description": "First row" }
]
}
]
}
}- Send raw base64 data in
content.data(withoutdata:<mime>;base64,prefix). - Make sure
mimetypematches payload bytes (image/png,video/mp4,audio/ogg, etc.). - Large payloads are limited by server body size (
50mbby default in this app). - For URL media, the URL must be publicly reachable by the server runtime.
- For quoted replies, the referenced message must be available in the session store.
Linux/macOS:
base64 -w 0 ./photo.jpgWindows PowerShell:
[Convert]::ToBase64String([IO.File]::ReadAllBytes(".\photo.jpg"))Session not connected: start session first withGET /session/start/:sessionIdand wait forready.chatId, contentType, and content are required: request body is missing required fields.Unsupported content type:contentTypemust be one of the documented enum values.- URL media send fails: URL is not reachable from server network, times out, or blocks bot user-agent.
- Group send fails: ensure
chatIdis correct@g.us, account is in group, and group allows participants to send. - Mention not visible: mentioned user ID must resolve to a valid WhatsApp user JID.
Events are delivered to BASE_WEBHOOK_URL with format:
{
"sessionId": "session1",
"dataType": "message_create",
"data": { ... }
}- Set
ENABLE_WEBHOOK=falseto disable outbound HTTP webhook delivery.
qr- QR code generatedready- Session connectedauthenticated- Authentication successfulauth_failure- Authentication faileddisconnected- Session disconnectedmessage- Incoming message (not from self)message_create- Any message (including sent)message_ack- Message delivery/read statusmessage_revoke_everyone- Message revoked for everyonemessage_reaction- Reaction to messagegroup_join- Participant joined groupgroup_leave- Participant left groupgroup_update- Group settings changedcall- Incoming callchange_state- Client state changedloading_screen- Loading/sync progress updatecontact_changed- Contact JID/identity changedchat_removed- Chat deleted/removedchat_archived- Chat archive state changedunread_count- Chat unread count updatedmedia_uploaded- Media upload eventremote_session_saved- Remote session persisted
Connect to:
ws://<host>:<port>/ws?apiKey=<API_KEY>&sessionId=<optionalSessionId>
- Set
ENABLE_WEBSOCKET=falseto disable websocket server startup. - If
sessionIdis provided, only that session's events are streamed - If omitted, all session events are streamed
- Message format:
{
"sessionId": "session1",
"dataType": "message_create",
"data": { "...": "..." },
"timestamp": "2026-02-12T10:20:30.000Z"
}- wwebjs uses
@c.usfor contacts:[email protected] - Baileys uses
@s.whatsapp.net:[email protected] - The API automatically converts between formats
- Message History: Message history is store-backed for synced and live messages. Full historical backfill beyond what the linked device syncs still depends on WhatsApp history sync.
- Contact List: Contact listing is store-backed from sync/live updates; unknown contacts are discovered progressively.
- Labels: Limited support for WhatsApp Business labels.
- Some metadata retrieval: Certain WhatsApp internals (e.g. deep business metadata) remain protocol-limited.
- Memory: ~500MB less RAM per session (no Chrome/Puppeteer)
- Speed: Faster connection and message sending
- Stability: No browser crashes or memory leaks
- Resources: Lower CPU usage
MIT