Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
669ff8d
fix(engine): match receipt keys across the lid dialect
rmyndharis Aug 20, 2026
8a3b6e4
fix(webhook): correct outcome accounting on the replay path
rmyndharis Aug 20, 2026
25d64be
docs(plugins): correct the PLUGIN_STATE_DIR migration note
rmyndharis Aug 20, 2026
ab673c1
Merge pull request #1414 from rmyndharis/fix/receipt-key-lid-dialect
rmyndharis Aug 20, 2026
15581a7
Merge pull request #1415 from rmyndharis/fix/webhook-replay-accounting
rmyndharis Aug 20, 2026
a37873c
Merge pull request #1416 from rmyndharis/docs/plugin-state-migration-…
rmyndharis Aug 20, 2026
1437fa9
feat(message): widen mentions to every text-carrying route
rmyndharis Aug 20, 2026
3a56bd1
fix(message): type the reply forwarder by its DTO
rmyndharis Aug 20, 2026
66d42f8
Merge pull request #1417 from rmyndharis/feat/mentions-on-every-text-…
rmyndharis Aug 20, 2026
b722a37
feat(mcp): accept mentions on the agent tools
rmyndharis Aug 20, 2026
e9a58c7
fix(engine): apply the mentions a sticker send accepts
rmyndharis Aug 20, 2026
8d40015
feat(mcp): accept customLinkPreview on the send-text tool
rmyndharis Aug 20, 2026
9797d78
Merge pull request #1419 from rmyndharis/fix/sticker-mentions-dropped
rmyndharis Aug 20, 2026
56a613b
Merge pull request #1418 from rmyndharis/feat/mcp-mentions-parity
rmyndharis Aug 20, 2026
34738ca
fix(mcp): tag a sticker send, and drop a rationale time made false
rmyndharis Aug 20, 2026
989294b
Merge pull request #1420 from rmyndharis/fix/mcp-sticker-mentions-and…
rmyndharis Aug 20, 2026
6b78690
chore(release): v0.23.0
rmyndharis Aug 20, 2026
09f9c6a
chore(sdk): release 0.5.0 across all five SDKs
rmyndharis Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -689,9 +689,11 @@ PLUGINS_DIR=./data/plugins # Plugin directory (default: ./data/plugins)
# this one.
# Moving it does NOT carry the existing state across: point it at an empty directory and the
# gateway starts with no record of installed plugins, even though their packages are still under
# PLUGINS_DIR. Move the whole `<old root>/plugins` tree yourself when you change this, not just
# registry.json: each plugin's persisted ctx.storage lives beside it as `<plugin id>/key-*.json`,
# so carrying the registry alone leaves every plugin enabled but with its state gone.
# PLUGINS_DIR. Copy `registry.json` and every `<plugin id>/key-*.json` from `<old root>/plugins`
# into `<new root>/plugins` when you change this: carrying the registry alone leaves each plugin
# enabled with its persisted ctx.storage gone. Copy those files rather than the directory, because
# on the default layout `<old root>/plugins` IS PLUGINS_DIR, and moving it would take the installed
# packages away from the loader.
# PLUGIN_STATE_DIR=./data
# Cap on a plugin .zip downloaded by install-from-URL (matches the 5 MB upload limit). A non-positive
# or non-numeric value falls back to the default.
Expand Down
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.23.0] - 2026-08-20

### Added

- `POST /sessions/{sessionId}/chats/read` takes an optional `messageIds` array (up to 100) naming which messages to acknowledge. Baileys acknowledges individual messages, so without it a burst left its earlier messages unread. Ids resolve through the message store, so a group receipt carries its `participant`. Available on the agent tool and all five clients; ignored by whatsapp-web.js. Thanks @m7fz7.

### Changed

- The agent tools accept `mentions` on every send whose engine carries it (text, the four media sends, sticker, template and reply) and `customLinkPreview` on the text send, matching the REST routes. A tool schema is not strict, so an agent that passed either field before had it dropped without an error.
- `mentions` reaches every route whose engine can carry it: `reply`, `edit`, `send-template` and each `send-bulk` item, alongside the send routes that already had it. `send-template` also gained `linkPreview`. On `edit` the tags are re-applied rather than preserved, because an edit replaces the message content. Thanks @Magnarks for the report.
- `POST /sessions/{sessionId}/chats/unread` publishes its own `MarkChatUnreadDto` rather than sharing `MarkChatReadDto`. The body is unchanged (`chatId` alone), but a generated client sees the schema under a new name.
- ⚠️ **Breaking (Go, Java and typed Python callers).** `markRead` and `subscribePresence` each take their own request type rather than the shared `MarkChatRequest`, which now serves `markUnread` alone. Swap the type at both call sites; the wire body is unchanged and the JavaScript and PHP clients are unaffected. `SubscribePresenceDto` had no contract-gate coverage while one type stood for two routes.
- ⚠️ **Breaking (Go, Java and typed Python callers).** `markRead` and `subscribePresence` each take their own request type rather than the shared `MarkChatRequest`, which now serves `markUnread` alone. Go and Java need the swap at both call sites; typed Python only at `markRead`, its `subscribePresence` body being structurally identical. The wire body is unchanged, and JavaScript and PHP are unaffected.

### Fixed

- `POST /messages/send-sticker` applies the `mentions` it accepts. The route shares `SendMediaMessageDto` and docs/06 lists it among the media sends that take the field, but both adapters built the sticker content without a tag list, so a documented capability did nothing on either engine.

- `POST /chats/read` answers 400 for `"messageIds": null` instead of 500. `@IsOptional` skips every validator for null as well as undefined, so the value reached the Baileys adapter and was dereferenced there. The published schema now carries `minItems` too, so it no longer advertises an empty array the server refuses.
- A read receipt goes only to the chat the caller named. A message id belonging to another chat in the same session carried that chat's address out of the message store, so the receipt landed there while the route reported success for the chat in the path.
- The Go client can express an empty `messageIds` again. `omitempty` on a plain slice dropped it, so a caller asking for nothing to be acknowledged silently acknowledged the newest message; the field is a pointer, so absent and empty are distinct on the wire.
Expand All @@ -27,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Restoring a backup no longer aborts when the target already holds the outbound delivery records. The table has no session foreign key, so the replace never cleared it and every overlapping row collided, rolling the whole import back.
- Settled outbound delivery records are pruned after `WEBHOOK_OUTBOX_RETENTION_DAYS` (default 7). A record that can still be replayed is never pruned on age, and a non-positive window falls back to the default rather than letting the table grow without bound.
- `PLUGIN_STATE_DIR` moves the plugin registry and per-plugin storage off the default `./data`. It was the one piece of state with no path knob, so a test run rewrote the developer's own registry.
- `backup.sh` and `restore.sh` follow `PLUGIN_STATE_DIR`. Both hardcoded the plugin state under the data dir, so with the knob set the archive carried neither the registry nor any plugin's persisted storage, and a restore put nothing back. The knob's own note now says to move the whole tree, not just `registry.json`.
- `backup.sh` and `restore.sh` follow `PLUGIN_STATE_DIR`. Both hardcoded the plugin state under the data dir, so with the knob set the archive carried neither the registry nor any plugin's persisted storage, and a restore put nothing back. The knob's own note now spells out which files to carry across when the knob changes.
- The e2e lane sweeps the throwaway state roots it creates. Each suite gets its own, nothing removed them, and the temp directory accumulated hundreds of entries over a few days of runs.
- e2e assertions are no longer answered by unrelated processes on the host. supertest binds its per-request listener to the wildcard address and then dials 127.0.0.1, which on macOS lets a process holding that port on 127.0.0.1 answer instead. Each suite's server now listens on loopback during init, which supertest reuses.
- A stalled `apt-get` can no longer hold a CI run open. The scripts-smoke job installed sqlite3 and shellcheck unbounded, so a slow mirror held two main runs past an hour with every other job already green. Both steps now time out and skip the install when the runner already ships the tool.
Expand Down
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ socket. Security matters here, and we appreciate responsible disclosure.

## Supported versions

Security fixes land on the latest minor release (currently 0.22.x). Older minor
Security fixes land on the latest minor release (currently 0.23.x). Older minor
lines receive no backports — please upgrade older deployments.

| Version | Supported |
| ------- | ------------------ |
| 0.22.x | :white_check_mark: |
| < 0.22 | :x: |
| 0.23.x | :white_check_mark: |
| < 0.23 | :x: |

## Reporting a vulnerability

Expand Down
4 changes: 2 additions & 2 deletions charts/openwa/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: openwa
description: OpenWA — WhatsApp API. Single-instance StatefulSet (see values.yaml replicaCount warning).
type: application
version: 0.1.16
appVersion: '0.22.0'
version: 0.1.17
appVersion: '0.23.0'
keywords:
- whatsapp
- openwa
Expand Down
32 changes: 18 additions & 14 deletions docs/06-api-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ There is a **single shared media byte cap**, not a per-type table. A base64 (or

### Mentions

`send-text` and the media send routes accept an optional `mentions` array of WIDs (`<phone>@c.us`) to tag participants — most useful in groups. Two things are required for WhatsApp to render a tag and notify the participant:
`send-text`, the media send routes, `send-template`, `send-bulk` (per item), `reply` and `edit` all accept an optional `mentions` array of WIDs (`<phone>@c.us`) to tag participants — most useful in groups. On `edit` the tags are re-applied rather than preserved: an edit replaces the message content, so a rewritten body that still reads `@62811` loses the tag unless the list is sent again. Two things are required for WhatsApp to render a tag and notify the participant:

1. The `mentions` array lists the WID(s), e.g. `["62811@c.us"]`.
2. The `text`/`caption` contains the matching `@<number>` token, e.g. `Hello @62811`.
Expand Down Expand Up @@ -1571,8 +1571,8 @@ rejected with `400` rather than guessing which half was meant.
Nine `send-*` routes accept an optional `quotedMessageId`: `send-text` above, and `send-image`,
`send-video`, `send-audio`, `send-document`, `send-sticker`, `send-location`, `send-contact` and
`send-poll` below. Supplying it turns that send into a reply, so a reply can carry media, a location,
a contact card or a poll — not only text. `POST .../messages/reply` is unchanged and remains the text
shorthand.
a contact card or a poll — not only text. `POST .../messages/reply` remains the text shorthand, and
like the send routes it accepts `mentions`.

`send-template`, `send-bulk` and `send-product` do NOT accept the field, and reject it
as an unknown property.
Expand Down Expand Up @@ -1626,6 +1626,8 @@ Render a stored text template (header/body/footer joined by blank lines, `{{vars
| templateId | string | Conditional | non-empty; required when `templateName` is absent | Stored template id |
| templateName | string | Conditional | non-empty; required when `templateId` is absent | Stored template name |
| vars | Record\<string,string\> | No | object | Substituted into `{{placeholder}}` tokens; defaults to `{}` |
| mentions | string[] | No | array of WIDs | WIDs to @mention in the rendered body |
| linkPreview | boolean | No | — | Same engine split as `send-text`; see **Link previews** |

```json
{
Expand Down Expand Up @@ -1919,11 +1921,12 @@ Reply to a message, quoting a prior message.

**Request body** — `ReplyMessageDto`

| Field | Type | Required | Constraints | Description |
| --------------- | ------ | -------- | ----------- | --------------------------------------- |
| chatId | string | Yes | non-empty | Target chat |
| quotedMessageId | string | Yes | non-empty | WhatsApp id of the message being quoted |
| text | string | Yes | non-empty | Reply text |
| Field | Type | Required | Constraints | Description |
| --------------- | -------- | -------- | ------------- | ---------------------------------------- |
| chatId | string | Yes | non-empty | Target chat |
| quotedMessageId | string | Yes | non-empty | WhatsApp id of the message being quoted |
| text | string | Yes | non-empty | Reply text |
| mentions | string[] | No | array of WIDs | WIDs to @mention. See **Mentions** above |

```json
{ "chatId": "628123456789@c.us", "quotedMessageId": "true_628123456789@c.us_3EB0ABCD", "text": "Replying to you" }
Expand Down Expand Up @@ -2055,11 +2058,12 @@ Edit the text of a message sent by this account; also updates the stored record'

**Request body** — `EditMessageDto`

| Field | Type | Required | Constraints | Description |
| --------- | ------ | -------- | ----------------------- | ------------------------------------------------- |
| chatId | string | Yes | non-empty | Chat containing the message |
| messageId | string | Yes | non-empty | Message to edit (the send response's `messageId`) |
| body | string | Yes | non-empty, ≤ 4096 chars | New text content |
| Field | Type | Required | Constraints | Description |
| --------- | -------- | -------- | ----------------------- | ------------------------------------------------- |
| chatId | string | Yes | non-empty | Chat containing the message |
| messageId | string | Yes | non-empty | Message to edit (the send response's `messageId`) |
| body | string | Yes | non-empty, ≤ 4096 chars | New text content |
| mentions | string[] | No | array of WIDs | Re-applies participant tags to the new body |

```json
{ "chatId": "628123456789@c.us", "messageId": "true_628123456789@c.us_3EB0ABCD", "body": "Corrected text" }
Expand Down Expand Up @@ -2095,7 +2099,7 @@ Send messages to multiple recipients as an async batch — returns immediately a
| messages | BulkMessageItemDto[] | Yes | array, max 100, nested-validated | The batch items (see below); duplicate `chatId`s are collapsed before processing — first occurrence wins, order preserved |
| options | BulkMessageOptionsDto | No | nested-validated | Pacing/error options (see below) |

Each `BulkMessageItemDto`: `{ chatId: string, type: 'text'|'image'|'video'|'audio'|'document', content: BulkMessageContentDto, variables?: Record<string,string> }`. `content` (all fields optional, nested-validated): `text?: string`, `image?`/`video?`/`audio?`/`document?`: `{ url?, base64?, mimetype?, filename? }`, `caption?: string`.
Each `BulkMessageItemDto`: `{ chatId: string, type: 'text'|'image'|'video'|'audio'|'document', content: BulkMessageContentDto, variables?: Record<string,string> }`. `content` (all fields optional, nested-validated): `text?: string`, `image?`/`video?`/`audio?`/`document?`: `{ url?, base64?, mimetype?, filename? }`, `caption?: string`, `mentions?: string[]` (per item; a batch fans out to many chats, and a WID is only taggable in a chat the participant is in).

`BulkMessageOptionsDto`: `{ delayBetweenMessages?: number (1000–60000, default 3000), randomizeDelay?: boolean (default true), stopOnError?: boolean (default false) }`.

Expand Down
1 change: 1 addition & 0 deletions docs/14-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ docker compose run --rm openwa-api npm run migration:run:prod

| Release | Change | Action |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0.23.0` | Typed SDK clients: `markRead` and `subscribePresence` each take their own request type instead of the shared `MarkChatRequest`, which now serves `markUnread` alone | Go and Java: swap the type at both call sites. Typed Python: only at `markRead`, its `subscribePresence` body being structurally identical. JavaScript and PHP need no change; the wire body is unchanged |
| `0.22.0` | Baileys refuses a reply whose quoted id, or a forward whose `fromChatId`, does not name the addressed chat, with the `404` whatsapp-web.js already answered; leaving a group, unsubscribing from a channel and labelling a channel surface WhatsApp's refusal; membership requests for an id that is not a group are refused | Handle a refusal on those six calls, which previously answered `200` whatever happened |
| `0.22.0` | Typed SDK clients narrow their request bodies: 19 Python request types mark the fields the server requires, and Go and Java type the proxy scheme, call kind, membership method, chat state, pin window and status font as enums | Pass the named constants instead of bare strings or numbers and supply every required field; untyped callers are unaffected |
| `0.22.0` | `isReadOnly` on a group answers for the calling account rather than repeating the group setting, and `isMyContact` reflects whether the contact is actually saved | Re-read either field wherever logic branched on the old value |
Expand Down
Loading
Loading