Added
-
CHAT_MEDIA_ARCHIVE_OUTBOUNDarchives media this account sent — a sub-flag ofCHAT_MEDIA_ARCHIVE_ENABLED(off by default), so a sent attachment gets the same durable file copy, S3 portability and TTL retention inbound media already had. Refs #1165. -
Group membership requests: list, approve, reject, and a
group.join_requestevent — the join-approval queue wwebjs/Baileys both expose is now surfaced:GET/POST .../groups/:groupId/membership-requests[/approve|/reject]on both engines, plus a webhook/socket event when someone asks to join an administered group. Refs #1164 (discussion). -
Own global presence:
PUT /sessions/:id/presence— appear online or offline on both engines; an always-online headless bot suppresses the phone's own notifications, andavailable: falsehands them back. Connection-scoped (re-issue after a reconnect). Refs #871. -
Blocklist read:
GET /sessions/:sessionId/contacts/blocked— the missing read half of the block/unblock endpoints, as a bare array of neutral contact ids on both engines; on Baileys an unanswered query is a503, never an empty list. -
Upstream capability-surface drift guard —
scripts/check-upstream-surface.mjs(intest:scripts) diffs the installed engines' Client/socket methods and event maps against a reviewed snapshot, so an engine bump that ships new capabilities fails CI until the delta is reviewed instead of sitting unnoticed. -
The media download route now serves media sent by the account —
GET /messages/:chatId/:messageId/mediafalls back to the inline copy stored on the message row when no archived file exists, which covers outbound messages (never archived) and inbound messages whose archived file retention has purged.
Changed
-
⚠️ Breaking (behavior). Engine operations during a WhatsApp Web page reload answer
409instead of a raw500— WhatsApp Web reloads its own page (measured ~5 minutes after a fresh pairing, among others) and whatsapp-web.js re-injects into it; for that bounded window every engine route now answers the documented retryable409naming the reload, where it previously surfaced rawTypeError500s — and the six chat write routes (read/unread/archive/unarchive/typing/clear) answered200 {success:false}. Retry after the session re-emitsready. Typed 4xx also no longer count toward the send breaker, so a reload can no longer latch its 15-minute cooldown. -
GET /sessions/{id}/chatsnow documents and answers the503a dead page deserves — the one read route that still surfaced a page-transport death as a raw500, now split exactly like its siblings. -
125 routes now document a status they could already answer — 91 gained the
409an unconnected session answers, plus400on six sends,403on six group and channel writes,404on eleven, and501on eleven more. A generated client had no branch for any of them. -
Five routes now document a
503they have been answering for releases — the four group participant writes since 0.14.5, and listing chats by label since 0.14.0. There the503is what separates "WhatsApp never answered" from the per-participant refusals a200reports insideresults.
Fixed
-
A URL-based send no longer discards bytes the gateway already downloaded — merging its metadata onto the engine's own-send echo replaced that echo's real payload with the URL string, so a whatsapp-web.js URL send rendered as a bare marker after a reload.
-
A bulk media send lost its attachment when the engine echo won the persist race — the batch write collided on
UNIQUE(sessionId, waMessageId)and was swallowed into a warning, leaving only the echo's row, which on a Baileys API send carries a media-less marker; it now merges onto that row like the single-send path already did. -
A WhatsApp Web page reload no longer kills a starting session — a navigation landing during the first injection rejects
Client.initialize()before whatsapp-web.js has installed its own re-inject handler, and the adapter's error channel is terminal, so one reload parked the sessionFAILEDwith nobody retrying (on start and on reconnect alike); the engine now retries that launch once, within the init deadline, before declaring failure. -
The liveness watchdog no longer tears down a session that is healing itself — a post-
readypage navigation makesgetState()reject until WhatsApp Web reboots, which read as two failed probes and a teardown of a page whatsapp-web.js was about to re-inject; the probe now grants a bounded post-navigation grace (never for a logout navigation, and capped per episode so a reload loop cannot mask a genuinely dead page). -
Three shared status descriptions said the wrong thing on the routes that borrowed them —
send-text's501is specifically a caller-suppliedcustomLinkPreview, which only Baileys accepts, not a wholesale gap in the engine;POST /channels/subscribetakes an invite code and no channel id, so its404is an unresolvable invite; and the400on six send routes named the unreachable recipient as though it were the only cause, while omitting body validation and an inactive session, and cited a404meaning that four of those six routes do not declare. -
Four contract corrections from a review of this cycle's own work — the
409description said the session had no engine when the guard actually fires on an engine that exists but is notready(an unstarted session answers400);send-bulkdeclared a409it cannot produce, since the batch drains after the handler has answered202; the nine catalog and status routes declared that409but not the404their own service throws for an unstarted session; and the logout example showed a nullpushNameandconnectedAtbeside a populatedlastActive, though logout clears onlyphone. -
Fifteen statements in the codebase that were not true — comments, a test name and an architecture sketch still described the own-send echo by its pre-0.10.0 behaviour: that whatsapp-web.js echoes carry no media, that the engines are at parity in carrying none, and that the echo never writes the database.
-
An API key's session allow-list showed ids that can never match —
allowedSessionsis matched by exact equality against the session id, but the example was['session-uuid-1', 'session-uuid-2'], which would scope a key to nothing and deny every request. It now shows real UUIDs. -
Seven published examples showed values the API cannot produce — a
sess_-prefixed session id thatParseUUIDPiperejects, two truncated UUIDs, a logout example missing the requiredengineLoaded, a readiness probe keyed ondatabaserather thanmainDatabase/dataDatabase, asend-textcapability no engine declares, and the whatsapp-web.js id form on a Baileys-only route.