You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Autoreply rules — per-session single-message autoreplies under /api/sessions/:id/automation-rules; conditions use the webhook filter format, the reply goes through the normal send path, and fromMe/freshness/per-chat-cooldown guards bound reply loops.
Message & chat management — pin/unpin and star/unstar messages, archive/unarchive chats, clear a chat without deleting it, and vote on polls (whatsapp-web.js).
Contacts, groups & channels — save/edit/remove addressbook contacts; read/set/remove a group picture; memberAddMode group setting; preview a group from its invite code before joining; create/delete/mute channels.
Labels — create, rename, recolour and delete labels; eight label agent tools for MCP (four read-only, four write).
Presence & calls — subscribe to presence (presence.update, online/typing) and receive call-outcome events (call.accepted, call.rejected, call.missed).
Send options — linkPreview toggle on send-text, plus a caller-supplied customLinkPreview on Baileys.
Media & status — server-side media conversion (audio→Ogg/Opus, video→MP4) via ffmpeg (MEDIA_CONVERSION_ENABLED); post an audio status as a voice note; archive chat media to the file store and fetch it back after delivery (CHAT_MEDIA_ARCHIVE_ENABLED).
Opt-in send pacing (SEND_PACING_ENABLED) — warm-up ramp, daily caps, a failure breaker, and a cold-reachout budget that also bounds group participant adds; enforcement is recorded in the audit log.
Account restrictions surfaced — WhatsApp-imposed restrictions appear on the session (API + session.restriction webhook + dashboard badge) instead of a generic error.
SDK coverage for the new surface — the JavaScript, Python, Go, Java and PHP SDKs all gained this release's new calls: pin/star a message, poll voting, archive and clear a chat, labels, channels, presence subscription, group picture / join-info / member-add mode, addressbook contacts, server-side media conversion and voice status. Autoreply rules are REST-only for now.
Horizontal-scaling groundwork (opt-in) — sessions record their owning process via a renewed lease so two replicas cannot both start one; a node only reaps the in-flight bulk batches of sessions it may claim, and a data import reports the sessions another node is running; a dead node's sessions are adopted automatically once the lease lapses (SESSION_TAKEOVER_SWEEP_MS); session-scoped requests are forwarded to the owning node when each node sets NODE_URL; and WebSocket events fan out across replicas when REDIS_ENABLED=true.
Changed
Built under the full TypeScript strict family, with per-module test-coverage floors across the codebase.
The official Docker image now installs ffmpeg (~210 MB larger, measured with --no-install-recommends). It backs the opt-in media-conversion endpoints and is installed unconditionally, so the binary is present even though MEDIA_CONVERSION_ENABLED still defaults to off. It is the Debian package rather than a bundled static build so codec CVEs arrive through the same security stream as the rest of the image.
session.restriction is now socket-subscribable as well as webhook-delivered, and the dashboard session card picks up a restriction (or its lift) live instead of only on a page reload.
⚠️Breaking (behavior). Eager status backfill on session ready is now opt-in (STATUS_SEED_ON_READY, default off): the immediate status@broadcast read could make some freshly paired whatsapp-web.js accounts lose the companion at WhatsApp Web's first scheduled reload. Statuses posted before a session connects are therefore no longer backfilled unless you set STATUS_SEED_ON_READY=true; live status events are unaffected either way. Thanks @duckvhuynh.
⚠️Breaking (behavior). Link previews are opt-in on the Baileys engine. Sends carrying a URL previously produced a preview card because the engine's own generator ran by default; they now go out without one unless the request passes linkPreview: true (or a customLinkPreview). This restores the documented engine default and removes a blocking outbound fetch from every URL-bearing send — which cost a bulk campaign minutes — but recipients of an unchanged integration will see plain links where a preview card used to appear.
Fixed
Status posting on the whatsapp-web.js engine — current WhatsApp Web had broken text and media status outright; the postinstall patcher restores both. Text, image, video and voice status post again.
whatsapp-web.js sessions could report "ready" with a dead inbound pipeline after a warm restart, silently dropping incoming messages. The adapter now refuses to promote a session whose event bridge never attached, reloading the page once and failing loudly (keeping credentials) if it stays dead.
Baileys delete-chat, mark-unread and delete-for-me silently did nothing on individual (1:1) chats — the neutral id was not folded to the engine form used as the app-state key.
Voice notes on the Baileys engine now carry a waveform.
The webhook producer enqueues idempotently, and the bundled Redis is pinned --maxmemory-policy noeviction so queued jobs are not silently dropped.
A media-storage root the app cannot write to is caught at boot with a clear error, instead of failing on the first write (#1066).
The postinstall hook no longer aborts with EALLOWSCRIPTS under npm 11 when the user's .npmrc sets allow-scripts=true: npm exports that as npm_config_allow_scripts, which npm 11 refuses in the nested dashboard install. The hook now strips it from the environment passed to each nested step. Thanks @configurowebmax.
The bundled docker-compose.yml now forwards the SEND_PACING_*, MEDIA_CONVERSION_*/FFMPEG_PATH and session-ownership (NODE_ID, NODE_URL, lease/sweep) variables — previously these features could not be enabled from .env in a compose deployment at all.
The four label write tools (LabelUpsert, LabelDelete, LabelAddToChat, LabelRemoveFromChat) answered Internal error over MCP even though the write had succeeded on WhatsApp, prompting agents to retry a completed operation; they now return { success: true }.
Boot validation covers the lease and routing knobs. A heartbeat that is not comfortably under half the lease TTL (renewals landing after the claim lapsed, so peers adopt sessions from a healthy node), a NODE_URL that is scheme-less or carries embedded credentials, a non-integer AUTOMATION_MAX_PER_SESSION, and a non-positive media-conversion knob are now boot errors instead of silent fallbacks or a 500 on the first forward. Forwarded responses also relay the owner's Retry-After/X-RateLimit-*, and an unusable owner URL answers 503 rather than 500.
Autoreply rules gain a per-session cap (AUTOMATION_MAX_PER_SESSION, default 32, 0 = unlimited), mirroring the webhook fan-out cap's shape (WEBHOOK_MAX_PER_SESSION, default 16): every inbound message is evaluated against every rule of its session.
The cold-reachout probe recognises a bare phone number, so a known contact passed without a JID suffix is no longer charged as a stranger; a voice status accepts backgroundColor; and a graceful shutdown no longer logs a handful of Redis unsubscribe errors.
Security (multi-node routing only): the session forwarder could be aimed at any origin. HTTP/1.1's absolute-form request target (GET http://elsewhere/api/sessions/x) is matched by the router and left verbatim in the request URL, where resolving it against the owner's address discards that address — an authenticated caller could make a node forward their request, API key attached, to an origin of their choosing and read the response. The forward target is now rebuilt from the owner's origin plus the request's path and query, so the destination cannot be influenced at all. Deployments without NODE_URL (the default) were never affected: the forwarder is inert there.
stop and delete are fenced against a session a live peer is running. Only start was claim-checked, and neither of those two needs a local engine, so a request landing on the wrong node — routine when session ownership is configured but request routing is not — wrote disconnected over a peer's live session or deleted its row and credentials while the peer's engine kept running. Both now answer 409; a lapsed claim still proceeds, since taking over is what the claim rule allows.
Multi-node ownership races closed: a stop landing while a start is mid-launch no longer hands back the claim under a live engine (which left the engine unclaimed and startable a second time elsewhere), a failed start no longer pins its session to that node forever, and a deliberate teardown of a session whose crashed owner's lease had lapsed now really leaves it down instead of being re-adopted by the takeover sweep. Successful forwarded requests also stop logging a spurious error.
The send breaker only counts failures that reached WhatsApp. Client-fault and engine-state errors raised inside the send call — a blocked media URL, a capability the engine lacks, a disconnected socket, a malformed status post — no longer accumulate toward it, so a client sending bad requests can no longer 429 every send on a healthy session for the cooldown. Applied to single sends, bulk batches and status posts alike.
send-sticker with a video mimetype works in the official image. whatsapp-web.js converts video to animated WebP through ffmpeg, which the image did not ship, so a documented-as-supported call always failed there; the image now carries the binary.
Backup/restore covers automation_rules. The table was in neither the export nor the import while the restore's session wipe cascade-deletes it, so a documented backup→restore silently destroyed every autoreply rule.
Media conversion answers 400 (not 500) for a blocked, unreachable or oversized input URL; POST /api/sessions/:sessionId/channels/:channelId/mute refuses a non-channel id instead of muting an ordinary chat forever; listing a label's chats and previewing an unknown group invite answer 404 instead of 500 on whatsapp-web.js; a refused disappearing-message change answers 403; voice-status media is served as audio rather than octet-stream; PUT /labels/:id treats explicit null fields as an empty body; and an addressbook write with a bare phone number is qualified before it reaches Baileys.
Small correctness batch: the cold-reachout history probe matches both user-id spellings (a known contact addressed the other way no longer reads as cold), an expired account-restriction stops badging the session, ending a ringing call clears its live handle, PUT /labels/:id refuses an empty body, addressbook writes refuse group/newsletter/broadcast ids (not just @lid), and the channel endpoints' OpenAPI docs state the real 403 refusal status instead of 422.
SDKs: the Go SDK encodes a nil poll-vote options as [] (clearing a vote now works from the zero value), the Java GroupSettings and SendVoiceStatusRequest records keep back-compat constructors after gaining a field, and all five SDKs expose the voice-status backgroundColor the server accepts.
The dashboard status viewer now plays a voice status with an audio player instead of rendering it as a broken image, the JS SDK's StatusRecord.type includes 'voice', and the SDK README's method tables and three misplaced doc comments were brought back in line with the actual surface.
Fetching a status's media that S3 retention already removed answers 404 instead of 500 (the local-storage miss already did), and server-side media conversion is bounded to MEDIA_CONVERSION_CONCURRENCY (default 2) concurrent ffmpeg processes with a short queue — saturation answers 503 instead of stacking processes.
Baileys message-action targeting: star/pin/unpin/react/delete now verify the stored message belongs to the requested chat (a mismatched pair answered success while writing under the wrong conversation), pin/unpin/react/delete resolve LID-migrated contacts like every other send, and tagging/untagging a chat's label folds the neutral id to the engine form so the write lands on the real chat instead of a phantom index.
whatsapp-web.js raw-id extraction hardening: every raw WhatsApp Web id is now read through one helper that accepts the renamed property, so a minified WA Web build no longer breaks id-bearing reads. Listing a label's chats no longer 500s when a labelled chat was deleted; the channel list, channel creation and group-invite preview stop returning ids as the literal string "undefined" or turning a valid invite preview into a false 404; the number-registration check (GET /api/sessions/:sessionId/contacts/check/:number) stops reporting every number as unregistered; and group participants, the group owner and the "am I admin" comparison resolve again instead of yielding "undefined" ids.
Baileys refusals now answer their documented statuses instead of 500: an invalid/expired group invite previews as 404, admin-refused group writes (subject, description, announce/locked, picture, member-add mode) and refused channel writes (create/delete/mute) answer 403 — matching the whatsapp-web.js engine. Transport failures still propagate unchanged.
Multi-node request routing hardening: forwarded requests now carry the client address in x-forwarded-for (so allowedIps keys and per-IP throttling see the real client once peer nodes are listed in TRUSTED_PROXIES), a malformed session id no longer 500s on Postgres in routed mode, and the client-settable forwarded marker is verified — a marked request landing on a live non-owner answers 409 instead of executing there.
Send pacing accounting: forwards now pass through the cold-reachout gate (they were ungated while still draining the budget), replying to someone who wrote first today no longer spends the cold budget, and a pacing 429 inside a bulk batch is recorded as SEND_PACING_LIMITED instead of a generic SEND_FAILED.
Multi-node: a session claim could outlive its engine and pin the session to one node forever. A failed start, a logout, a force-kill or an exhausted reconnect kept the ownership claim, and the heartbeat renewed it indefinitely — the session could not be started on any peer and the takeover sweep never saw it. Claims are now released on every teardown path, and the heartbeat only renews claims that still cover a live engine, an in-flight start or a pending reconnect. Starting a nonexistent session also answers 404 again instead of a misleading 409 "running on another node".
Corrected column names in docs/05-database-design.md.
Documentation refresh: capability-matrix and MCP tool counts, the sessions ownership columns and the automation_rules table in the database doc (both now covered by the schema-accuracy gate), the socket-subscribable event list, and precise wording for send-pacing scope and failover engine-overlap.
Security
Resolved five advisories in the production dependency tree via transitive bumps (brace-expansion, fast-uri, hono, ip-address, socket.io-parser); no direct dependency changed.