Skip to main content

Sessions

Manage WhatsApp sessions. Each session represents a connected WhatsApp account.

Create Session​

Creates a new session and automatically starts connecting to WhatsApp.

POST /api/v1/sessions

Request Body​

{
"id": "my-session",
"name": "My Business Account",
"webhook": {
"url": "https://example.com/webhook",
"events": ["message", "connected"],
"secret": "webhook-secret"
},
"device": {
"os": "Windows",
"platform": "desktop"
}
}
FieldTypeRequiredDescription
idstringNoCustom session ID (auto-generated if not provided)
namestringNoFriendly name for the session
reusebooleanNoRe-scan an existing session ID instead of failing — see below
webhookobjectNoWebhook configuration
webhook.urlstringYes*Webhook URL
webhook.eventsarrayNoEvents to subscribe (default: all)
webhook.secretstringNoHMAC secret for signature verification
deviceobjectNoPer-session device identity override (see Device Identity)
device.osstringNoOS label shown in WhatsApp Linked Devices, e.g. Windows, Mac OS X, Ubuntu
device.platformstringNoPlatform type — see table in Device Identity
device.versionstringNoDotted app version, e.g. 2.3000.1023902713. Omit to use library default

Reusing an existing session ID (reuse, v0.12.0+)​

By default, creating a session with an ID that already exists returns 409. Pass "reuse": true to instead keep the original session_id and re-start the connect flow (QR/pair re-scan) on the same slot — the response is 200 with the existing session. name and webhook fields are ignored on reuse; update those via their own endpoints. reuse has no effect when the ID does not exist yet.

Response​

{
"session": {
"id": "my-session",
"name": "My Business Account",
"phone_number": null,
"push_name": null,
"status": "connecting",
"created_at": 1767143203,
"updated_at": 1767143203,
"last_connected_at": null,
"is_logged_in": false
}
}

Example​

curl -X POST http://localhost:3451/api/v1/sessions \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "business-1",
"name": "Business Account",
"webhook": {
"url": "https://example.com/webhook"
}
}'

List Sessions​

Get all sessions.

GET /api/v1/sessions

Response​

{
"sessions": [
{
"id": "my-session",
"name": "My Account",
"status": "logged_in",
"is_logged_in": true
}
],
"total": 1
}

Get Session​

Get a specific session by ID.

GET /api/v1/sessions/{session_id}

Response​

{
"id": "my-session",
"name": "My Account",
"phone_number": "628123456789",
"push_name": "John Doe",
"status": "logged_in",
"is_logged_in": true
}

Delete Session​

Delete a session and everything stored for it. This cannot be undone. To unlink the phone and keep the session, use Log Out.

DELETE /api/v1/sessions/{session_id}

Cascade behaviour​

Session delete cascades on both storage layers:

  • DB rows — child rows in webhooks, contacts, and webhook_dlq scoped to this session_id are dropped explicitly before the sessions row is removed. This runs even on databases whose tables were migrated in without an ON DELETE CASCADE constraint, so no orphans are left behind.
  • In-memory registry — every webhook registration pointing at this session is removed from the dispatcher, along with any open circuit state for those URLs.
  • Storage directory — the on-disk session store under WHATSAPP_STORAGE_PATH/{session_id} is unlinked.

Response​

{
"success": true,
"message": "Session deleted"
}

Get Session Status​

Get current connection status.

GET /api/v1/sessions/{session_id}/status

Response​

{
"status": "logged_in",
"is_logged_in": true,
"socket_alive": true,
"paused": false,
"phone_number": "628123456789",
"push_name": "John Doe",
"diagnostics": {
"last_data_received_at": 1759140000,
"client_frames_received": 5120,
"client_messages_received": 212,
"client_messages_suppressed_duplicate": 0,
"client_events_dropped": 0,
"client_reconnects": 1,
"client_reconnect_errors": 0,
"messages_forwarded": 212,
"last_message_forwarded_at": 1759139990,
"webhooks": [
{
"id": "wh_1",
"url": "https://example.com/hook",
"enabled": true,
"receives_messages": true,
"circuit_open": false,
"consecutive_failures": 0,
"retry_in_seconds": null,
"last_error": null,
"skipped_while_suspended": 0
}
],
"new_chats": {
"last_3h": 2,
"last_6h": 5,
"last_12h": 9,
"last_24h": 14
}
}
}
FieldDescription
socket_aliveRaw socket liveness (v0.12.0+). Distinct from is_logged_in: a cached logged_in status can outlive a dead socket ("limbo"), and a live socket can precede login during QR/pair flows.
pausedtrue after Pause Session — the session is deliberately offline until resumed.
diagnosticsWhere inbound traffic stops (v0.13.2+); see below.

Diagnosing "connected but no webhooks"​

diagnostics follows a message from the socket to your webhook, so you can see which step stopped:

CheckMeans
last_data_received_at is oldthe socket has gone quiet
client_messages_received doesn't grow while messages arrivethe client isn't decrypting or dispatching
client_messages_received grows but messages_forwarded doesn'twaxum isn't forwarding
messages_forwarded grows but your server gets nothinglook at webhooks[]: circuit_open: true (suspended after failed deliveries; last_error says why and retry_in_seconds when it is tried again), enabled: false (you disabled it), or receives_messages: false
  • client_* fields come from the WhatsApp client. They are null when no client is running in this process, and reset whenever it is rebuilt (e.g. by connect).
  • messages_forwarded counts since the process started.

Status Values​

StatusDescription
disconnectedNot connected
connectingEstablishing connection
waiting_for_qrWaiting for QR code scan
waiting_for_pair_codeWaiting for pair code entry
connectedConnected but not logged in
logged_inFully authenticated

Get QR Code​

Get QR codes for authentication.

GET /api/v1/sessions/{session_id}/qr

Response​

{
"qr_codes": ["2@ABC123..."],
"timeout_seconds": 60,
"status": "waiting_for_qr"
}

Connect Session​

Manually trigger connection (usually not needed as create auto-connects).

POST /api/v1/sessions/{session_id}/connect

Request Body (optional)​

Accepts an optional device identity override. Same shape as device in Create Session. The override only takes effect on the first successful pair — subsequent reconnects reuse the props that whatsapp-rust persisted at pair time.

{
"device": {
"os": "Windows",
"platform": "desktop"
}
}

Empty body is fine — falls back to environment defaults (WA_DEVICE_OS, WA_DEVICE_PLATFORM, WA_DEVICE_VERSION).

:::tip Lifts an account-lock cooldown When WhatsApp locks an account, auto-reconnect pauses with an escalating cooldown (see the account_locked webhook event). A manual POST /connect is the intended way out — it clears the cooldown and reconnects immediately. :::


Pair with Phone Number​

Connect using pair code instead of QR.

Only for a session that is not linked yet. A session that already holds a WhatsApp login gets 409 (since v0.13.6 this holds even while it is reconnecting); use POST .../connect to bring it back online.

POST /api/v1/sessions/{session_id}/pair

Request Body​

{
"phone_number": "+628123456789",
"show_push_notification": true,
"device": {
"os": "Windows",
"platform": "desktop"
}
}

device is optional. See Device Identity for the field schema and the available platform values.

Response​

{
"code": "ABCD-EFGH",
"timeout_seconds": 60
}

Disconnect Session​

Close the connection to WhatsApp. The session stays linked: connect brings it back without a new QR.

POST /api/v1/sessions/{session_id}/disconnect

Log Out​

Since v0.14.0. Remove waxum from the account's linked devices, and keep the session.

POST /api/v1/sessions/{session_id}/logout

The session becomes dormant: its id, webhooks, settings and stored messages are kept, only the WhatsApp login is dropped. connect then shows a fresh QR, and scanning it (or pairing by code) continues the same session, with the same number or a different one.

400 on a whatsapp_cloud session, which has no linked device.

Dormant sessions​

A session is never deleted unless you call Delete Session or the bulk purge. In particular:

  • When WhatsApp logs the device out (removed from the phone, or the account is restricted), the session goes dormant the same way as after logout. Up to v0.13.6 a repeated logout deleted the session together with its webhooks and history.
  • A dormant session reports status: "disconnected", is_logged_in: false.
  • Its webhooks are loaded at startup like any other session's, so they are in place the moment it is linked again.

New-chat Limit​

Since v0.14.0. Linked-device sessions only.

WhatsApp restricts accounts that start too many conversations with numbers they have never talked to, and unlinks every companion device when it does. The threshold is not published and differs per account. waxum counts what it can see, records the count whenever WhatsApp logs the session out, and lets you set a ceiling.

A chat counts as new when the session sends to a direct chat it has no history with: no earlier send through waxum and no stored message in either direction. Groups never count, and neither does replying to someone who wrote first.

GET /api/v1/sessions/{session_id}/settings/new-chat-limit
PUT /api/v1/sessions/{session_id}/settings/new-chat-limit
DELETE /api/v1/sessions/{session_id}/settings/new-chat-limit

Set a limit​

{
"max_new_chats": 40,
"window_hours": 24
}
FieldTypeRequiredDescription
max_new_chatsintegerYesNew chats allowed inside the window. At least 1.
window_hoursintegerNoRolling window, 1 to 720. Defaults to 24.

No limit is set by default, and nothing is refused until you set one. DELETE removes it; the counting continues either way.

When the limit is reached​

A send to a number the session has no chat with is refused:

HTTP/1.1 429 Too Many Requests
Retry-After: 3120
{
"success": false,
"error": {
"code": 429,
"message": "New-chat limit reached: 40 new chats per 24 h. Existing chats are not affected. Retry in 3120 s."
}
}

Sends in existing chats, replies and group sends keep working, as do edits, reactions and revokes. A send that fails is not counted. Sends through NATS follow the same rule and get a failed send_result.

Response​

All three methods return the current state:

{
"limit": { "max_new_chats": 40, "window_hours": 24 },
"new_chats": { "last_3h": 2, "last_6h": 5, "last_12h": 9, "last_24h": 14 },
"remaining": 26,
"retry_after_seconds": null,
"incidents": [
{
"occurred_at": 1791422622,
"reason": "LoggedOut",
"new_chats": { "last_3h": 31, "last_6h": 44, "last_12h": 52, "last_24h": 61 }
}
]
}

remaining is how many new chats the limit still allows inside its own window, never below 0, and null when no limit is set.

incidents holds the figures at each logout by WhatsApp, newest first. After a few of them you can read this account's threshold off the numbers and set the limit below it. The same figures are in diagnostics.new_chats of Get Session Status, and in the account_locked webhook event.

caution

waxum counts only what is sent through it. Chats started from the phone or WhatsApp Web on the same account are not included, so the number WhatsApp acts on can be higher than the one reported here.

Check whether a chat exists​

GET /api/v1/sessions/{session_id}/chats/{jid}/exists

jid is a phone number or a JID. The answer says whether the session already has a chat with it, and whether a send there would count as a new chat:

{
"jid": "628111111111@s.whatsapp.net",
"exists": false,
"counts_as_new_chat": true
}

A chat exists when the session started it through waxum or has any stored message with it, in either direction. A phone number is resolved to the contact's LID when the session is connected, since history may be stored under either. Groups, newsletters and broadcasts never count.

This answers from what waxum has stored. It does not ask WhatsApp whether the number is registered; use Check on WhatsApp for that. A number that is not on WhatsApp still counts as a new chat when a send to it goes through waxum.


Get Device Info​

Get connected device information.

GET /api/v1/sessions/{session_id}/device

Response​

{
"device_id": 1,
"phone_number": "628123456789",
"lid": "123456789@lid",
"push_name": "John Doe"
}

Session Tags​

Free-form short strings an operator attaches to a session to organise a fleet — e.g. cs, blast-campaign-2, client:acme, region:jkt. Added in v0.7.13.

:::note In-memory, not a DB table Tags live in-memory on the running instance and are snapshotted to {WHATSAPP_STORAGE_PATH}/session_tags.json on every mutation, so a restart reloads them from that file rather than losing them. They are not stored in the SQL database alongside sessions/messages/etc. Not on the send hot path — tags are only consulted when ?tag= is present on List Sessions, or by the console's overview grouping. :::

List Session Tags​

GET /api/v1/sessions/{session_id}/tags

Response​

{
"session_id": "my-session",
"tags": ["cs", "region:jkt"]
}

Replace Session Tags​

Overwrites the full tag set for a session.

PUT /api/v1/sessions/{session_id}/tags

Request Body​

{
"tags": ["cs", "region:jkt", "client:acme"]
}

Response​

Same shape as List Session Tags.

Add Session Tag​

Adds a single tag without disturbing the rest of the set.

POST /api/v1/sessions/{session_id}/tags

Request Body​

{
"tag": "priority"
}

Response​

{
"session_id": "my-session",
"tag": "priority",
"changed": true,
"tags": ["cs", "region:jkt", "priority"]
}

changed is false when the tag was already present.

Remove Session Tag​

DELETE /api/v1/sessions/{session_id}/tags/{tag}

Response​

Same shape as Add Session Tag, with changed reflecting whether the tag was actually present to remove.

List All Tags​

Every distinct tag across the fleet, with how many sessions carry it.

GET /api/v1/tags

Response​

[
{ "tag": "cs", "session_count": 8 },
{ "tag": "region:jkt", "session_count": 3 }
]

Sorted by session_count descending, then alphabetically.

Filter Sessions by Tag​

List Sessions accepts a ?tag= query parameter to restrict results to sessions carrying that tag:

GET /api/v1/sessions?tag=region:jkt

Deleting a session also drops its tags from the in-memory registry and the JSON snapshot.


Session Export / Import​

Move a session's local device identity and credentials between waxum instances — an explicit, operator-triggered migration. Added in v0.9.3.

:::warning Not multi-instance failover whatsapp-rust's storage is local-SQLite-only; it has no networked storage backend. Export/import is a manual, one-shot copy — not transparent failover or live replication between instances. WhatsApp also does not allow the same device credentials to be live on two places at once, so export always disconnects the source session first. :::

Export Session​

Packages the session's local storage directory (device identity, Signal protocol keys, noise handshake state — everything whatsapp-rust itself persists to disk) as a zip and streams it back.

POST /api/v1/sessions/{session_id}/export

Disconnects the session first — the underlying device credentials must never be live on two waxum instances simultaneously, so export always leaves the source session disconnected as a side effect, even if it was logged_in a moment earlier.

Response​

200 with Content-Type: application/zip and a Content-Disposition: attachment; filename="{session_id}.waxum-session.zip" header. The body is the zip archive — not JSON.

Import Session​

Restores a session's local storage directory from an export zip, e.g. after copying it to a different waxum instance.

POST /api/v1/sessions/{session_id}/import

The target session id must already exist (create it first if needed). Upload as multipart/form-data with the zip in a field named file:

curl -X POST http://localhost:3451/api/v1/sessions/my-session/import \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@my-session.waxum-session.zip"

Response​

{
"success": true,
"message": "Session storage imported — call /connect to bring it online"
}

Notes:

  • Refuses to run (409) if the target session is currently connected on this instance — disconnect it first, or import into a fresh session id.
  • Does not auto-reconnect. Call Connect Session afterwards to bring it online.
  • Zip entries are validated against path traversal ("zip-slip") before extraction; unsafe entries abort the import with 400.

Device Identity​

Controls how each session appears in WhatsApp's Linked Devices list at pair time (the OS string + platform icon). The default is Windows / desktop, which displays as WhatsApp Desktop rather than a browser client.

Where it applies​

  • POST /api/v1/sessions — device field in the create body
  • POST /api/v1/sessions/{id}/connect — optional body with a device field
  • POST /api/v1/sessions/{id}/pair — device field alongside phone_number

Important caveat​

Device props are only honored on the first pair. Once a device is registered with WhatsApp, the gateway persists the props in its SQLite store and reuses them on every reconnect. To change the identity of an already-paired session, delete it and pair again.

Field schema​

FieldTypeDefaultDescription
osstringWindowsFree-form OS label shown in WA
platformstringdesktopOne of the platform values below
versionstringlibrary defaultDotted version like 2.3000.1023902713. Leave unset unless you have a specific reason — forcing a version has been observed to cause silent server-side drops on freshly-paired sessions.

Platform values​

ValueLinked Devices icon
desktopWhatsApp Desktop (default)
uwpWindows Store app
chromeGoogle Chrome
firefoxMozilla Firefox
edgeMicrosoft Edge
safariSafari
operaOpera
ieInternet Explorer
ipadiPad
ios_phoneiPhone
android_phoneAndroid Phone
android_tabletAndroid Tablet

Unknown values fall back to desktop.

Environment fallback​

When no device field is sent, the gateway falls back to these env vars:

WA_DEVICE_OS=Windows
WA_DEVICE_PLATFORM=desktop
WA_DEVICE_VERSION= # omit for library default