A major feature- and security-focused release. Adds six dashboard languages and a
real-time Chats view, completes the outgoing-message and delivery-state webhook
story, introduces message templates and live chat history, hardens the API surface,
session lifecycle, and container runtime, and upgrades the WhatsApp engine. See
Upgrade notes for the behavior changes.
Added
- Dashboard / Chats: A new real-time Chats view — browse a session's
conversations, stream incoming and outgoing messages live over WebSocket, send
text and media, and mark chats as read. Thanks @akbarxleqi (#152). - Dashboard / i18n: Six new languages on a single canonical language picker —
Simplified Chinese, Traditional Chinese, Arabic (full RTL), Telugu, French, and
Italian — alongside the existing English and Hebrew. The picker now also appears
on the Login screen and resolveszh-Hant/HK/MO/TWregional variants. Thanks
@jr-everstar (#150), @7odaifa-ab (#145), @abhinayguduri (#149), and
@albanobattistella (#224). - Messages: Server-side message templates with
{{variable}}substitution —
full CRUD under/sessions/:id/templatesplus a
POST /sessions/:id/messages/send-templateendpoint that renders and sends.
Text templates only; interactive buttons/list/HSM are not supported on the
whatsapp-web.js engine. Thanks @esakarya (#69). - Messages:
GET /sessions/:id/messages/:chatId/historyreads chat history live
from WhatsApp (bypassing the local DB), with optional base64 media;limitis
clamped to 1–100. Thanks @jgalea (#96, closes #162). - Groups: Group payloads now expose
linkedParentJID— the JID of the parent
community a sub-group belongs to. Thanks @ferhatte10 (#201). - Webhooks:
message.sentnow fires for every outgoing message — including
messages composed on a linked phone (via the whatsapp-web.jsmessage_create
event), not just messages sent through the API. (closes #93, #168, #195) - Webhooks / Sessions: Stored message status now reflects real delivery state
from acks —delivered,read, andfailed— advancing monotonically (a late
or out-of-order ack can never downgrade a higher status). A send that never
receives a delivery ack stayssent, so it is visibly "not delivered" instead of
falsely "sent". A newmessage.failedwebhook is emitted on an error ack so
consumers can detect non-delivery without polling. Independently identified and
prototyped by @aminebalti55 (#225). (closes #155, #199, #220) - Webhooks: Opt-in outbound SSRF protection — set
WEBHOOK_SSRF_PROTECT=trueto
refuse webhook URLs that resolve to loopback, private, link-local, CGNAT, or
cloud-metadata addresses (default off). (#221) - API:
BODY_SIZE_LIMITcaps request body size (default 25 MB, sized for
base64 media sends).ENABLE_SWAGGERgates the/api/docsUI (default on; set
falseto disable it on exposed deployments). (#221, #67) - Webhooks:
message.receivedpayloads now include the group sender's identity
—author(the participant WID) andcontact{ name, pushName }. Additive and
backward compatible. (#223, closes #146) - Sessions: Opt-in auto-start of previously authenticated sessions on boot via
AUTO_START_SESSIONS=true(default off); sessions start sequentially to bound
Puppeteer memory and one failure does not block the others. Thanks @mayko7d
(#135, closes #218). - Sessions:
PUPPETEER_EXECUTABLE_PATHpoints the engine at a system
Chromium/Chrome binary (for Alpine, ARM, or custom base images); unset keeps
Puppeteer's bundled Chromium. (#219) - Docs: Community integrations page documenting the community-maintained
ioBroker adapter (with a not-endorsed caveat). (#223, closes #134)
Changed
- Engine: Upgraded
whatsapp-web.jsfrom 1.26.1-alpha.3 to 1.34.7
(improved LID handling and stability). (#222) - Dashboard: Responsive layout for small screens and improved dark-mode
contrast across pages; the Plugins page no longer truncates the feature list.
Thanks @ashiwanikumar (#66). - Auth: The first-boot admin key is now a cryptographically random
owa_k1_
key in all environments by default; the fixeddev-admin-keyis seeded only
whenALLOW_DEV_API_KEY=trueis explicitly set. (#221) - Auth: Requests with a valid key but insufficient role now return 403
Forbidden instead of 401. (#221) - Docker / Podman: Base images are fully qualified (
docker.io/node:22-slim)
and the container healthcheck usescurl, so the image builds and runs under
Podman as well as Docker; added a Podman compatibility note to the docs. Thanks
@3bsalam-1 (#68). - Docs / API: Interactive messages (
Buttons/List) are documented as
unsupported on the whatsapp-web.js engine, and the speculative request-body
examples were removed from the API collection. (#223, closes #158)
Fixed
- Sessions: An engine operation attempted while a session is disconnected,
reconnecting, or still initializing (for example, refreshing the dashboard after
disconnecting the session from the phone) now returns 409 Conflict
("session not connected") instead of a 500 Internal Server Error. Thanks
@VincenzoKoestler for the related report. (#100) - Sessions: A terminal engine failure (Chromium failed to launch, or WhatsApp
rejected the stored credentials) now surfaces as afailedstatus with a
human-readable reason on the session and in the dashboard, instead of silently
closing the QR modal;auth_failureis treated as terminal rather than
triggering a reconnect loop. A status race that could revertqr_readyback to
initializingduring startup is also fixed. (#219) - Engine: The built-in engine plugin now honors
SESSION_DATA_PATHand the
configured Puppeteer settings instead of silently falling back to relative-path
defaults. (#219) - Infrastructure dashboard: Saved configuration (
data/.env.generated) now
applies reliably. The save handler wrote several env names the backend never read
(STORAGE_PATH,S3_ACCESS_KEY/S3_SECRET_KEY,ENGINE_HEADLESS/
ENGINE_SESSION_PATH/ENGINE_BROWSER_ARGS), so those settings silently reverted
to defaults on restart; they now match whatconfiguration.tsreads. Saving also
merges into the existing file instead of rewriting it from scratch, so a partial
save no longer blanks other keys or stored secrets, and the form hydrates from a
newGET /infra/configendpoint. Thanks @VincenzoKoestler (#226).
Security
- CORS: A wildcard (
*) origin is now refused in production (cross-origin
requests are blocked), and CORS credentials are only enabled with an explicit
origin allowlist. (#221) - WebSocket: A session-scoped API key can no longer subscribe to
*or to
sessions outside itsallowedSessionsallowlist, preventing cross-tenant event
leakage. (#221) - Authorization: Plugin enable/disable/config and the infrastructure read
endpoints (/infra/status,/infra/config,/engines,/engines/current,
/storage/files/count) now require an ADMIN key. (#221, #226) - Docker: The container reaches the Docker API through a least-privilege
docker-socket-proxyover TCP (DOCKER_HOST) instead of mounting the socket
directly, and the Node process runs as a non-rootopenwauser via agosu
privilege-dropping entrypoint (dumb-initstays PID 1 for clean signal handling).
Thanks @A831ARD0 (#227, #228; supersedes #129). - Health:
/api/healthis excluded from rate limiting so liveness probes do
not exhaust the limiter. (#221)
Dependencies
- CI: Upgraded
softprops/action-gh-releasev2→v3 and
docker/build-push-actionv6→v7 (both move the GitHub Actions runtime to
Node 24). (#169, #170)
Upgrade notes
- CORS in production: if you serve the dashboard on a different origin than the
API and relied on the defaultCORS_ORIGINS=*, setCORS_ORIGINSto the explicit
dashboard origin(s) — a wildcard is now refused in production. - Infrastructure reads are ADMIN-only:
/api/infra/status,/infra/config,
/engines,/engines/current, and/storage/files/countnow require an ADMIN key. - Role-denied requests return 403 (was 401) — update clients that branch on the
status code. - Not-ready engine ops return 409 (was 500) — clients calling group/chat/send
endpoints while a session is not connected now receive409 SESSION_NOT_READY. - First-boot key: non-production no longer seeds
dev-admin-keyby default (a
random key is generated and printed in the startup banner / written to
data/.api-key). SetALLOW_DEV_API_KEY=trueto restore the fixed local key. - Docker: the bundled Compose now runs a
docker-proxysibling and the API
talks to it viaDOCKER_HOST, and the container runs as non-root; review the new
Compose if you mounted the Docker socket directly or customized orchestration.