Added
- Hindi (हिन्दी) dashboard locale, selectable from the language picker. Thanks @probably-ABHINAV.
POST /api/auth/validatereturnsengineType, the engine the gateway runs, so every role can read it.
Fixed
- A reply, button click or quoting send made from a
message:receivedplugin hook stores the text of the message it quotes, so the dashboard shows that quote instead of an empty box. - Baileys: replies to and forwards of an edited message carry the edited text instead of the original.
- Baileys: a message deleted for everyone answers
404on reply, quoted send, forward, react, edit, star, pin, unpin and click-button; a reply or forward of it sent the deleted content back to the chat. - Baileys: a message deleted for everyone before the gateway finished processing it (while its media downloads, or replayed together with its delete on reconnect) is no longer announced with its content after
message.revoked, and does not become the chat preview. - Baileys: an edit that arrives while its message is still downloading is no longer lost:
message.received, the stored copy and the chat preview carry the edited text. - The dashboard dev server proxies only
/api/paths, so a full reload of the API Keys page (/api-keys) loads the dashboard instead of being forwarded to the backend. - The dashboard Chats thread renders an
@<digits>mention as@FirstNamewhen that participant has posted in the loaded thread and their id matches the digits in the body. This covers whatsapp-web.js, where the author id and the mention carry the same digits. On Baileys the author is normalized to the phone number when the lid mapping is known while the body keeps the lid digits, so those mentions stay as WhatsApp sent them, as does any mention of someone who has not posted. The resolved name renders inside its own<bdi>element, outside Linkify'signoreTags-respected walk, so a push name can never become a clickable link, however it's spelled. Thanks @TanmayChachra. - The dashboard Plugins page shows a plugin's status and type in the selected language; they rendered as raw English values (
installed,extension) in every locale. - On Baileys, quoting, reacting to, forwarding, editing or deleting a message the moment it arrives or is sent no longer fails intermittently with
Message <id> not found: a lookup of a message whose store write is still in flight now waits for that write. - On Baileys, a message WhatsApp re-delivers while its first copy is still being stored is no longer dispatched twice.
- Baileys: an edit, revoke or reaction that WhatsApp delivers inside a wrapper message is no longer dropped, so
message.edited,message.revokedandmessage.reactionfire for it. - Baileys: a WhatsApp-side unlink clears the session's stored messages and its persisted chat mute, archive and pin state, and an API logout now clears that state too, so a re-linked account no longer inherits the previous account's flags.
- Baileys: a failed chat-state read no longer resets a chat's persisted mute, archive and pin flags, and a chat list no longer queries the database once for every chat that has no stored state.
- Baileys: a chat muted "Always" reads as muted indefinitely instead of unmuted.
- Baileys: marking read or unread, clearing, archiving and deleting a chat WhatsApp addresses by the contact's lid work with the
@c.usidGET /chatsreturns, instead of answeringsuccess: false. - Baileys:
POST /api/sessions/:sessionId/chats/readwithoutmessageIdsacknowledges the chat's newest received message and answerssuccess: falsewhen none is known; after a reply sent from the phone it sent no receipt and still answeredsuccess: true. - Baileys: muting, pinning, labelling, starring and deleting for me a chat WhatsApp addresses by the contact's lid target that chat; mute and pin added a second row for it to
GET /chats. - Baileys: a chat deleted through the API or on the phone leaves
GET /chats, and its stored mute, archive and pin state is cleared. - Baileys: a session start reloads its chats' mute, archive and pin state from the database, so a change made on another node shows up.
- Baileys: a document sent from a URL whose host sends no Content-Type goes out as
application/octet-streaminstead ofapplication/pdf. - Baileys: a call whose rejection failed can be rejected again instead of answering
404, unless the session was disconnected meanwhile. - Baileys: two quick archive, mute or pin changes to a chat no longer lose one of the two.
- Baileys:
GETchannel and channel subscribe return the channel's name, description, invite code, subscriber count and verified flag, withcreatedAtas a number; onlyidand a stringcreatedAtcame back. - Baileys: delete for everyone on a message the account cannot revoke (another sender's message in a 1:1 chat, or in a group it does not administer) deletes it for the account only, as on whatsapp-web.js, instead of reporting success and deleting nothing. When the group's member list shows no row the gateway can identify as the account, the revoke is still sent.
- Baileys: the group list and group info recognise the account by its own lid in a lid-addressed group, so
isAdminandisReadOnlyare right there, and an admin's delete for everyone in such a group is sent as a revoke. - Baileys: the chat preview follows an edit or a delete of the chat's last message, including one made through the API and an inbound revoke.
- An image, video or audio sent by URL whose host answers with no Content-Type, or a generic
application/octet-streamone, goes out asimage/jpeg,video/mp4oraudio/mpeginstead of under that generic type, which whatsapp-web.js delivered as a document. - Baileys: a message sent through the API becomes the chat's last message, so chat lists sort and preview by it.
- Baileys:
DELETE /api/sessions/:sessionId/status/:idaddresses the revoke to the recipients the status was posted to; it was addressed to nobody before. - Baileys:
BAILEYS_LOG_LEVELdiagnostics keep an error's message and stack instead of loggingerr: {}. - Baileys: a catalog product listed without a price omits
priceandpriceFormatted, and one without a currency omitscurrency, instead of returningnullor aNaNprice, and a product card for it no longer sendsNaN. - whatsapp-web.js: stopping, deleting or logging out a session while Chromium is still launching no longer leaves a logged-in browser running that nothing owns.
- whatsapp-web.js: a session that reported connected keeps its credentials when the readiness deadline fires right after the event-bridge reload, instead of being forced to pair again.
- whatsapp-web.js: a chat history read with a zero, negative or non-numeric limit returns the default 50 messages instead of every loaded message, and a plugin's
ctx.engine.getChatHistorycoerces a non-numeric limit the same way. - whatsapp-web.js: button, list and template-button replies arrive as type
text, as on Baileys, instead ofunknown, and rows stored with the old tokens are backfilled at boot. Rows earlier releases stored asunknownkeep that type. - whatsapp-web.js: posting a status no longer downloads its own media a second time from the echo event.
- whatsapp-web.js: the auto-resolved WhatsApp Web pin is refreshed daily instead of held for the life of the process, where it eventually pointed at a build the registry had deleted.
- A
WWEBJS_AUTH_TIMEOUT_MSabove about 24.8 days no longer fails every session start with504. - The lid-to-phone cache keeps the most recently written mappings after a restart instead of evicting them first.
GET /api/infra/engineslists channels, status updates and catalog among the Baileys features.- A session named after another session's id no longer moves that session's WhatsApp credentials onto itself at boot.
- A session with a reconnect base delay above about two minutes now reconnects while the liveness watchdog keeps reporting its wedged engine, instead of having the reconnect pushed back on every report.
- A session that recovers on its own after the liveness watchdog scheduled a reconnect is no longer torn down when that reconnect fires.
- A
nullmaxReconnectAttemptsorreconnectBaseDelayin aPOST /api/sessionsconfig means the default (unlimited attempts, 5000 ms) instead of turning auto-reconnect off. - Message history keeps a revoke or edit that arrives while the
message:receivedhook chain is still running, instead of storing the original content. API_MASTER_KEYis trimmed before the first admin key is seeded, so a trailing newline no longer seeds a key that can never authenticate.- On a multi-node deployment,
GET /api/sessionsand the MCP session tools reportengineLoaded: truefor a session another live node runs, so the dashboard offers Stop, Logout and Force-kill for it instead of Start. WithNODE_URLset on every node those actions are forwarded to the owner; without it only the owner can act on the session. - With
RESOLVE_LID_TO_PHONE=true, one transient lookup failure no longer stops a sender's phone from ever being resolved again. - Bulk media sent as base64 without a mimetype is stored with the mimetype it was sent with, so the media endpoint serves it, and a bulk media URL without a mimetype takes the fetched Content-Type instead of a hardcoded one.
- Bulk sends pass the recipient
chatIdin themessage:sendingandmessage:failedhook input, as single sends do. - Deleting a session during a large history sync no longer leaves orphan message rows, and deleting a session removes its stored statuses and chat mute, archive and pin state at once.
GET /api/sessions/:sessionId/presence/:chatIdanswers JSONnullwhen nothing is reported, as documented, instead of an empty body.- Forwarding to a node whose
NODE_URLhas a path prefix keeps the prefix. - Product sends count toward the send failure breaker like every other send.
GET /api/plugins/:id/healthreports a sandboxed plugin whose worker crashed, failed to enable or is disabled as unhealthy.- Removing a row from a plugin config array of masked secrets keeps each remaining secret on its own row. Where the rows cannot be told apart without their secrets (bare secrets, or rows that differ only by a secret), a removal is refused with a request to re-enter the remaining values, instead of keeping the removed secret and dropping a kept one. Deleting one row and adding one in the same save keeps the list's length and is still read as an in-place edit.
- With
QUEUE_ENABLED=true, ingress deliveries that share a provider delivery id across instances or plugins are no longer dropped as duplicates. - The ingress reconciler no longer stalls on pending rows of a disabled or deleted instance, and an ingress job failed by BullMQ stall exhaustion writes a dead-letter row and fires
ingress:errorinstead of being lost. - The ingress reconciler no longer counts a stranded delivery as delivered when its queued job had already failed; the dead-letter row stays redrivable.
- An integration instance created without a
verifyTokengets a generated one, as documented, so a GET verification handshake can succeed; the dashboard shows it once when the instance is created. ctx.storage.list()no longer returns the plugin package'smanifestandpackagefiles as storage keys, and an in-place plugin update keeps legacy (pre-encoding) storage files.GET /api/plugins/cataloganswers400instead of500for a catalog with a non-object entry.- A plugin instance PATCH whose config is rejected, or whose save fails, changes nothing, instead of leaving
enabledchanged. PUT /api/sessions/:sessionId/webhooks/:idanswers400instead of500whenurl,events,headers,activeorretryCountis null.- Webhook deliveries drop connection-level custom headers (
Connection,Content-Length,Expect,Keep-Alive,TE,Trailer,Transfer-Encoding,Upgrade);Expect,Keep-Alive,Transfer-Encoding,Upgradeor a wrongContent-Lengthmade every delivery fail. - Values saved from Dashboard > Infrastructure that contain
#, quotes or leading or trailing spaces are written so the next boot reads them unchanged; a database, Redis or S3 password with a#was truncated, which could keep the gateway from starting. - Restoring a data backup reloads the cached chat mute, archive and pin state instead of serving and later rewriting the pre-restore values.
- Switching from external S3 to built-in MinIO creates MinIO with the credentials the restarted gateway uses.
- With Redis-backed rate limiting,
Retry-Afterreports when the block lifts instead of the full block duration. - The REST API, Bull Board,
GET /api/healthand MCP accept theBearerscheme in any letter case. - PostgreSQL boot migrations no longer inherit
DATABASE_STATEMENT_TIMEOUT_MS, so a long migration during an upgrade is not cancelled at 30 seconds. PUPPETEER_ARGSkeeps a comma inside a flag value, such as--window-size=1280,720.- Boot validation catches
MAIN_DATABASE_NAMEpointing at the data database's default SQLite file. - The production weak-secret refusal names
S3_ACCESS_KEY_IDandS3_SECRET_ACCESS_KEYinstead of the legacy variable names. - Boot fails on a non-integer
STORAGE_EXPORT_TTL_MSorSTORAGE_EXPORT_SWEEP_MAX_AGE_MSand on a timer above 2147483647 ms forSTORAGE_EXPORT_TTL_MS,MESSAGE_REAPER_INTERVAL_MS,WEBHOOK_RECONCILE_INTERVAL_MSandINGRESS_RECONCILE_INTERVAL_MS; such values deleted export archives within milliseconds or fired every millisecond. - Updating an automation rule with a
nullname,replyText,cooldownSecondsorenabled, or an integration instance withenabled: null, answers400instead of500. GET /api/auditanswers400for a repeated or unknownactionorseverity, and breakscreatedAtties by id so pages never overlap.- The top-chats statistics no longer label a group with one member's name; a group's
chatNameisnull. - Automation rules without a
kindcondition no longer auto-reply to channel posts, broadcast-list messages or status updates. scripts/restore.shworks on the compose named volume and the Helm PVC:OPENWA_RESTORE_SNAPSHOT_DIRmoves the pre-restore snapshot off the read-only container root, and the restore runbook gives the in-image commands.scripts/restore.shtakes every pre-restore snapshot before it writes anything, and copies what a symlinked data dir points at; the snapshot of a symlinked data dir was a link to the live data the restore then overwrote.scripts/restore.shrestores a state directory that is its own mount point in place, instead of emptying it and then failing to re-create it.scripts/restore.shremoves leftover SQLite-wal,-shmand-journalfiles before restoring a database; a stale write-ahead log brought the replaced rows back.scripts/restore.shplaces databases, sessions, media and plugins where the archive's.env.generatedpoints, since that file replaces the target's.scripts/backup.sharchives a symlinked sessions, Baileys, media or plugins directory by its content, andscripts/restore.shrefills it through the link instead of replacing the link.- The Helm chart renders whole-number
envandsecretEnvvalues from a values file as integers instead of exponent notation such as5.24288e+07. scripts/backup.shandscripts/restore.shread.envlines with CRLF endings or spaces around=as the app does, follow the app's fallback from a leftoverSTORAGE_LOCAL_PATH=./uploadsto./data/media, handle the admin key atBOOTSTRAP_KEY_FILE, and warn about missing media and legacy./pluginscode the archive does not carry.scripts/restore.shstops before replacing any database when a target it would write cannot be written, instead of leaving a half-restored install.- The bundled compose files forward
DATABASE_SSLandDATABASE_SSL_REJECT_UNAUTHORIZED, so TLS to a managed PostgreSQL set in.envtakes effect. docker-compose.dev.ymlpublishes the API onAPI_PORT, as.env.exampledocuments.- Dashboard Chats no longer shows the previous session's chat list when that list arrives after a session switch, and changing the UI language no longer resets Chats to the first session or closes the open chat.
- Dashboard Chats: a message sent or received while a chat's first page is loading no longer disappears when the page lands, and text typed next to an audio attachment stays in the input instead of showing as sent, since audio carries no caption.
- Dashboard Chats offers no reply, react or delete on a message that is not sent yet, which could only fail.
- Dashboard Chats lists a chat as soon as its first message arrives instead of after a reload, and a failed background refresh of the chat list keeps the list on screen instead of emptying it.
- Dashboard Chats: a document's caption shows in its bubble, a file the browser cannot type is sent as a document instead of failing with
400, sends without a message id no longer replace each other, a send that finishes after a session switch no longer reorders the other session's list, and a search result in a session that is not connected shows a warning instead of an empty, mislabeled session. - Dashboard: message types and chat kinds read as words in the selected language in the reply banner and quote, the chat list, the messages-by-type chart, and the webhook filter tags and summary; a chat whose newest message is media no longer reads "No messages yet".
- Dashboard: the media viewer saves an image under its file name instead of its caption, and the chat header shows the right country code for three-digit codes and for +7 numbers.
- Dashboard: the audit CSV export retries a briefly throttled page, exports at most the newest 10,000 rows with a warning when there are more, keeps the rows fetched so far and asks to wait when the gateway keeps throttling the walk, and shows an error instead of quietly downloading only the current page.
- Dashboard: Infrastructure holds its form until the saved config has loaded, so Save can no longer overwrite the stored database, S3 and engine settings with defaults, and saving a built-in Postgres or Redis no longer stores a password the bundled container never receives.
- Dashboard: clearing a plugin instance's session scope returns it to all sessions instead of silently keeping the old scope, and a per-session plugin override the gateway rejects shows the error instead of "Saved".
- Dashboard: a newly linked session's card shows its phone and last-active time without a reload, and a slow QR answer no longer reopens a closed QR modal or replaces another session's QR.
- Dashboard: a failed background refresh keeps the last overview on screen instead of replacing the page with an error, and cancelling a bulk batch in Message Tester no longer flips back to Processing when a progress poll answers late.
- Dashboard: the new-session form refuses names shorter than 3 characters, and pressing Enter twice no longer sends two create requests.
- Dashboard: clearing an optional plugin config field clears the stored value, and the API Keys page shows a load or permission error instead of an empty list when the keys cannot be read.
- Dashboard: clearing a plugin's per-session override shows the Global values, so the next Save override no longer restores the cleared ones.
- Dashboard: turning off a built-in Postgres, Redis or MinIO on the Infrastructure page and restarting stops its container.
- Dashboard Sessions: an older list response no longer overwrites a newer status, the session detail modal follows status changes while open, and a pairing code no longer lands in a modal opened for another session.
- Dashboard: double-clicking Create no longer registers a webhook twice, an audit export whose search matches nothing says so, and call, order and product slices in the messages-by-type chart get their own colors.
- Dashboard: operator and session-scoped keys can post a status and open Channels, which depended on the admin-only engine route.
- Dashboard: Infrastructure keeps the form and the restart dialog on screen when a background status refresh fails, Templates and Message Tester report a failed sessions or groups read instead of an empty state, and the webhook Create button stays disabled until a session and URL are set.
- Dashboard: a search hit from another session opens that session's chat, a failed 'load more' in global search keeps the results already shown, a session status push right after a create or delete no longer drops or revives a card, and the audit export no longer repeats a row written during the walk.
- Dashboard: relative times use the singular or plural the count needs ("1 hour ago"), and Arabic status counts of 100 or more use the grammatical singular.
- Dashboard: the page search boxes show keyboard focus, the Infrastructure storage badge is translated, and the signed-in role is kept per browser tab with its API key instead of in storage shared by every tab.
- Dashboard: the session proxy settings are translated in 11 locales and the partial-export media warning in 10, together with the proxy Save button and the webhook chat-kind filter, the Telugu status composer and channel messages are translated, and counts that are exact multiples of a million render in the plural in French, Spanish, Italian and Portuguese.
- Dashboard: the Templates sidebar item is translated in Arabic, Hebrew, Telugu and both Chinese locales.
- JavaScript SDK: requests no longer fail with "Illegal invocation" in browsers and Workers, with or without an injected fetch;
timeoutMs0 orInfinityturns the timeout off instead of aborting every request after 1 ms, and a value that is not a non-negative number (an empty variable,30s) throws aTypeError; and an error body without the usual envelope is shown as JSON instead of[object Object]. - SDKs: a caller header that differs only in letter case from
X-API-KeyorContent-Typeno longer goes out next to the SDK's own value. - PHP SDK: the readiness
503throwsOpenWAServiceUnavailableExceptioninstead of an "Array to string conversion" error, and anhttp://base URL is logged witherror_log()instead of raising a PHP warning;allowInsecureHttpsilences it. - Java SDK:
CatalogProduct.priceis a nullableDouble, so a product without a price no longer breakscatalog.products(). - Go SDK: a
429or503whoseRetry-Afteroutlasts the remaining timeout is returned at once instead of sleeping into aTimeoutError. - Go SDK:
CatalogProduct.Priceis nil for a product without a price instead of reading as 0. - Go SDK: the opt-in retry policy does not retry a
429whose body carriescode: "SEND_PACING_LIMITED". - PHP SDK: the insecure-http warning fires for an upper-case
HTTP://base URL and no longer fires forhttp://LOCALHOST.
Documentation
- An n8n example forwards incoming WhatsApp messages to a Discord channel with n8n's built-in Webhook and HTTP Request nodes, without the OpenWA community node. Thanks @probably-ABHINAV.
- The API reference covers
engineLoadedon multi-node deployments, the new403,404,429and501answers, the connection headers dropped from webhook deliveries, the WebSocket subscribe limits,sessionScope: nullon instance PATCH, optional product price and currency, the production validation error body, the webhook delivery id and idempotency key shapes, node-local stats, and pinned install and catalog URLs. - The webhook signature snippets return
falseon a missing or malformed signature header instead of throwing. - README and the migration guide say a compose profile only starts its container and list the settings that point OpenWA at it, and the migration guide's permission advice, legacy PostgreSQL steps and rollback pointer are corrected.
- The troubleshooting guide no longer suggests running the container as a non-root user, maps a session that is not ready to
400or409, and no longer says the shipped compose file includes a TLS proxy; the n8n guide reads delivery-failure rows correctly. - The engine capability matrix says
ENGINE_TYPEselects one engine for the whole deployment and records the Baileys delete fallback, the Baileys status revoke403and the whatsapp-web.js501refusals. - The scaling guide says which session list and stats fields reflect the answering node, and the plugin guide documents the recipient
chatIdin bulk send hook input and the install pin rule. - The SDK READMEs and SDK reference create a session and pass its id, not its name, and
.env.examplesays a malformed or non-positive value for a boot-validated setting stops the gateway. - The maintenance runbook and the backup FAQ back up inside the container under the production compose, the version rollback reloads a PostgreSQL data store from the pre-upgrade dump, and the migration guide covers an upgrade from an image without
scripts/backup.sh. - The rollback and restore runbooks load a PostgreSQL dump into an empty database inside the built-in
openwa-postgrescontainer, and the backup docs keep the password out ofDATABASE_URLand say the local media directory is archived underSTORAGE_TYPE=s3too. - The SDK READMEs name the transient statuses, and the Go README states that a
POSTorPATCHis never retried after a network error and only on429or503. - The SDK docs say the rate limiter's
429carries its delay only inRetry-Afterand that aSEND_PACING_LIMITEDrefusal waitsretryAfterSeconds, and describeengineLoadedon multi-node gateways; the metrics reference lists its401,404and429in one place; the backup runbook and script headers give the real path order; and the upgrade hazards cover PostgreSQL upgrades from 0.19.0 to 0.21.x images, which ship nopg_dump. - The migration guide's SQLite-to-PostgreSQL script copies
chat_statesandwebhook_outbox_events, the storage exportdownloadfield is documented as a server-side path, the retention docs coverwebhook_outbox_events, live chat history is documented as oldest first, and theGET /api/infra/enginesexample shows real feature tokens.
Upgrade notes (behavior changes)
- Baileys:
DELETE /api/sessions/:sessionId/status/:idanswers403for a status this session did not post in the last 24 hours (one posted from the phone or from another node, or before the session's engine was last created by a restart, a stop and start, or a reconnect the gateway runs itself), because its recipients are unknown; it answered200without revoking anything. - whatsapp-web.js: a reply, location or contact card to a channel or a status/broadcast list, and a poll or sticker to a status/broadcast list, answer
501where they answered500; nothing is sent, and the send breaker no longer counts them. - whatsapp-web.js: adding a label id the account does not have answers
404where it answered200. POST /api/sessionsrefuses aconfigthat is not a JSON object with400.- Boot fails on a
DATABASE_TYPE,ENGINE_TYPEorSTORAGE_TYPEwith surrounding whitespace; on a malformed, zero or negative value for the chat-media, status and S3 re-probe settings (a negativeCHAT_MEDIA_ARCHIVE_TTL_DAYS; 0 still means keep forever); and on a timer above 2147483647 ms for the chat-media and status orphan sweeps,S3_REPROBE_INTERVAL_MSandMEDIA_CONVERSION_TIMEOUT_MS. Boot andmigration:runalso fail on SQLite whenMAIN_DATABASE_NAMEnames the data database file, including the default./data/openwa.sqlitewhenDATABASE_NAMEis unset; point one of them at a separate file.migration:runapplies the sameDATABASE_TYPErule. - whatsapp-web.js: button, list and template-button replies are type
textinstead ofunknownin webhooks, storage and message-type filters, and stored rows with the old tokens are rewritten at boot. Rows earlier releases stored asunknownkeep that type. - Baileys: reply, quoted send, forward, react, edit, star, pin, unpin and click-button on a message deleted for everyone answer
404where they answered200. - Java SDK:
CatalogProduct.priceis a nullableDoubleinstead of adouble; recompile against it and check fornullbefore unboxing. JavaScript SDK:CatalogProductprice,currencyandpriceFormattedare optional. - Go SDK:
CatalogProduct.Priceis a*float64instead of afloat64; check for nil before dereferencing. - An automation rule that should answer channel, broadcast-list or status messages needs an explicit
kindcondition; a rule without one skips those chats. scripts/restore.shrefuses an archive whose state directory was stored as a symlink by an olderbackup.sh; take the backup again with this version.- With
BOOTSTRAP_KEY_FILEset,scripts/restore.shwrites the admin key there and refuses to start when that path cannot be written. - Baileys: delete for everyone on a message the account cannot revoke now deletes it, with its media, for the account only and still answers
200; it used to change nothing. - Sessions deleted on 0.23.5 or 0.23.6 left their auth directories on the data volume, and upgrading does not remove them: delete the directories under the session data path and the Baileys auth dir whose session id no longer exists, drop backups that carry them, and remove the device under Linked Devices on the phone.
GET /api/plugins/:id/healthreports a sandboxed plugin that is disabled or whose worker crashed as unhealthy, so health-based alerting also fires for a plugin disabled on purpose.NODE_URLkeeps its path when a request is forwarded, so the path must be only a reverse-proxy prefix: aNODE_URLending in/apinow forwards to/api/api/....- With
QUEUE_ENABLED=true, ingress job ids change format. Let pending ingress rows drain (no row pending forINGRESS_RECONCILE_GRACE_MS) before upgrading, or a delivery replayed across the upgrade can be dispatched twice. - Integration instances created without a
verifyTokenbefore this release still have none; recreate one to get a generated token. - Compose installs whose
.envcame from a template older than 0.18 may still carry uncommentedDATABASE_SSL=falseandDATABASE_SSL_REJECT_UNAUTHORIZED=true. Compose now forwards both, so they override TLS settings saved in the dashboard; comment them out or delete them. The gateway logs a warning at boot when a forwarded value differs from the one saved in the dashboard. - A WebSocket
subscribewhosesessionIdis not*or a session id of at most 128 safe characters answersINVALID_SESSION, and a connection can hold at most 4096 subscriptions (TOO_MANY_SUBSCRIPTIONS). GET /api/metricsanswers429after 10 failed token attempts from one client within a minute.
Security
- Baileys: an inbound edit, revoke or reaction that targets a stored message from another chat, or an edit or 1:1 revoke from someone other than the author, is dropped. Any contact who knew a message id could rewrite or erase the stored copy of a message and trigger
message.editedormessage.revokedfor it. The check needs the original in the Baileys message store, so it does not cover messages imported by the history sync at link time or older than the newestBAILEYS_MESSAGE_STORE_LIMITmessages, and a group revoke is checked for the chat only, since an admin may revoke anyone's message. - Baileys: a message retry request is no longer answered with a stored message from a different chat; a requester whose lid neither the session nor Baileys can map to a phone number is still answered.
- A WebSocket
subscribewith an oversizedsessionIdretained about 20 MB of room names per frame, so any valid API key could exhaust the gateway's memory; the id is now validated and subscriptions per connection are capped. - Baileys: link previews scan the fetched page in linear time; a crafted page could stall the whole process for over a minute.
GET /api/metricsbounds failedMETRICS_TOKENattempts per client, so the token can no longer be guessed at full speed.- Plugin log metadata can no longer overwrite a log line's level, context, message, timestamp or trace.
- Backup archives under
./backupsand restore snapshots (*.pre-restore-*) are ignored by git and kept out of the Docker build context; they hold the admin API key, WhatsApp credentials and database copies. - Deleting a session removes its whatsapp-web.js and Baileys auth directories; they stayed on the data volume, and in every later backup, still linked to the WhatsApp account.
- Baileys: a text status containing a URL no longer reaches the library's own link-preview fetcher.
- The audit CSV export quotes a bare carriage return and neutralizes cells that start with a tab or carriage return as formula triggers.