Skip to content

feat: synchronize the node with OpenWA 0.23.3 - #45

Merged
rmyndharis merged 11 commits into
mainfrom
feat/sync-openwa-0.23
Aug 25, 2026
Merged

feat: synchronize the node with OpenWA 0.23.3#45
rmyndharis merged 11 commits into
mainfrom
feat/sync-openwa-0.23

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

The node was last extended against OpenWA v0.15.0. The server is now at v0.23.3, and the gap had two halves: capabilities that were never exposed, and shipped capabilities that had quietly stopped behaving correctly as the server evolved.

Correctness against a current server

All of these failed silently except the last, which returned a 400.

  • Trigger de-duplication keyed on the wrong field. v0.23.0 made outbound delivery crash-durable, replaying stranded deliveries under a stored idempotency key while minting a fresh deliveryId per attempt. The trigger de-duplicated on deliveryId, so the duplicate run the option exists to prevent still happened. It now keys on idempotencyKey, falling back to deliveryId for older gateways.
  • checkExists only probed for existence. A registration that had been deactivated, repointed at another URL, or resubscribed to a different event set reported healthy, leaving the trigger activated but permanently silent. It now compares active, url and events and rebuilds on drift.
  • Message Edit dropped mentions. An edit replaces the message content, so tags are re-applied rather than preserved; omitting the field stripped the @mentions of the message being edited and returned 200.
  • Chat Mark Read never sent messageIds. Baileys acknowledges individual messages, so a burst left its earlier messages unread and a restarted session had nothing to acknowledge.
  • Status Recipients was documented backwards. The note said an empty list broadcasts to everyone; whatsapp-web.js ignores the list entirely and broadcasts regardless, so a stated audience restriction was not one.
  • Template Update forwarded blank name/body, which the server refuses. Blank header/footer are still forwarded, since clearing them is the documented way to remove them.

New capability

The node goes from 101 to 157 operations, covering every session-scoped route the server publishes.

New resources: Presence, Media (server-side conversion), Automation Rule (gateway-side autoreply) and Catalog.

Filled-in resources: message pin/unpin/star/vote-poll/media-download/send-product plus quotedMessageId, wider mentions, link-preview control and a location address; chat archive/mute/pin/clear; contact addressbook writes and the blocklist read; label writes and the label-to-chats query; group membership requests, pictures and join preview; channel create/delete/mute/admin; session logout and config; call links; profile picture removal; voice statuses.

Server-side filters are the most consequential addition: the Trigger can now register them, so the gateway drops non-matching events instead of waking a workflow that would discard them.

Notable decisions

  • proxyType is not exposed. The server stores it, but neither engine reads it: the agent is chosen from the URL scheme, so the field would be a control that does nothing.
  • Send Product's response uses id where every other send uses messageId. It is passed through unchanged rather than normalised, so the difference stays visible.
  • Filters, headers and automation-rule conditions are JSON fields, matching how Webhook Update already modelled them.
  • Deliberately still absent: the server administration surface (infra, plugins, ingress, integration) and /api/metrics, which uses its own bearer token rather than this credential's API key.

Compatibility

The floor moves to 0.16.0, now set by the routes rather than the event catalog (Call > Create Link arrived in v0.16.0, above the 0.15.0 the event list needs). Four optional fields need a newer server and are documented in a table; they are opt-in, so leaving them alone keeps the node working at the floor.

No existing operation changes its route, method or body. Two behaviour changes worth naming:

  • Message Edit now sends mentions when supplied, which is the fix above.
  • On first activation after upgrading, a trigger whose registration had drifted will delete and recreate it. That is the intended repair, but it shows up as one round of registration churn.

Verification

npm run build, npm run lint, 409 tests (up from 299), and the n8n Creator Portal scanner all pass; the committed dist/ matches source.

A new test walks the node description and asserts every operation the UI offers reaches a handler branch, so an operation added to a resource list but forgotten in its handler now fails in CI rather than at runtime in a user's workflow. Route coverage was checked by executing every UI operation against a recording context and diffing the resulting URLs against openapi.json.

Several capabilities the node already ships stopped behaving correctly as the
server evolved. All but one failed silently, reporting success while doing
something other than what the workflow asked for.

* Trigger de-duplication keyed on `deliveryId`, which identifies a delivery
  attempt and is re-minted whenever the gateway replays a delivery stranded by
  a crash. Only `idempotencyKey` identifies the event, so the duplicate run the
  option exists to prevent still happened. It now keys on `idempotencyKey` and
  falls back to `deliveryId` for a gateway too old to send one.
* `checkExists` used its GET purely as an existence probe. A registration that
  was deactivated, repointed at another URL, or resubscribed to a different
  event set reported healthy, leaving the trigger activated but permanently
  silent. It now compares `active`, `url` and `events` and rebuilds on drift.
* Message Edit never sent `mentions`. An edit replaces the message content, so
  the tags are re-applied rather than preserved, and omitting the field stripped
  the @mentions of the message being edited.
* Chat Mark Read never sent `messageIds`. Baileys acknowledges individual
  messages, so a burst left its earlier messages unread and a restarted session
  had nothing to acknowledge.
* Template Update forwarded a blank `name` or `body`, which the server refuses
  with a 400. Blank `header` and `footer` are still forwarded, since clearing
  them is the documented way to remove them.
* The webhook secret guard enforced the 16 character floor but not the 255
  character ceiling, so an over-long secret failed at the server instead of
  naming the field.

Documentation carried the same drift. The Status Recipients note described the
allow-list as an opt-out broadcast on whatsapp-web.js; that engine ignores the
list entirely and posts to every contact, which made a stated audience
restriction misleading in a way that matters. Corrected alongside the catalog
and settings notes, which justified absent operations with server limitations
that no longer exist, and the role guidance, which omitted that four System
reads need an ADMIN key unrestricted by session.
The node already offered the `presence.update` trigger event, but nothing
could subscribe to a chat's presence, so the event could never fire. Presence
is now its own resource with Subscribe, Get and Set Own Presence.

All three are connection-scoped: the subscription and the account's own
availability live on the socket, so a restart or any automatic reconnect ends
them and nothing on the server re-issues them. The field copy says so and
points at driving them from a `session.status` branch rather than once at
workflow start. Subscribe additionally validates the chat id against the
domain-qualified form its DTO requires, which is stricter than every sibling
route, so a bare phone number is refused in the editor instead of at the server.

Session gains the three routes it was missing:

* Log Out, which asks WhatsApp to unlink the device and clears the stored
  credentials. Its description spells out how it differs from Stop and Delete,
  and that it needs a running session, so nobody puts a Stop in front of it.
* Get Config and Update Config. The update is a merge, and an absent key keeps
  its stored value. Returning an attempt limit to unlimited requires sending an
  explicit null, which a collection field cannot express, so -1 is accepted as
  that sentinel. Zero stays a real value meaning never reconnect.
* A Proxy URL on Create, which is the only point at which a session's egress
  proxy can be set. The scheme is checked in the node so a typo fails in the
  editor rather than as a Start that times out after 30 seconds with no QR.

Only `proxyUrl` is exposed. The server also stores a `proxyType`, but neither
engine reads it: the agent is chosen from the URL scheme, so offering the field
would be a control that does nothing.

An empty response body is no longer reported as `{ success: true }` for
anything other than a DELETE. Presence > Get answers 200 with no body when a
chat has reported nothing yet, and that means "nothing yet", not success.

A new test walks the node description and asserts every operation the UI offers
reaches a handler branch. An operation added to a resource's list but forgotten
in its handler previously failed only at runtime in a user's workflow.
Five operations the server has shipped for several releases had no equivalent
in the node, and five fields its send DTOs accept were never sent.

New operations:

* Pin and Unpin, with the three pin windows WhatsApp accepts offered as a
  dropdown, since any other value is refused. Unpin never carries a duration:
  its DTO does not declare the field, and an undeclared field is a 400.
* Star, whose `star` flag has no server-side default and is therefore always
  sent. Turning it off removes the star.
* Vote Poll. The selection is sent even when empty, because that is how a vote
  is cleared, and the wire field is `pollMessageId` rather than `messageId`.
  Selections are matched against the poll's own option texts, so the field
  description warns that a near-miss selects nothing and still reports success.
* Get Media, which downloads a message's stored media as binary. It is served
  from the gateway's archive rather than an engine, so it works while the
  session is stopped. It reuses the existing output-field parameter rather than
  adding a second one.
* Send Product. Its response is `{id, timestamp}` where every other send returns
  `{messageId, timestamp}`; the value is passed through unchanged rather than
  normalized, so the difference stays visible to the workflow.

New fields on existing operations:

* Quoted Message ID on the nine sends whose DTOs declare it, so a reply can
  carry media, a location, a contact or a poll instead of only text. An
  unresolvable ID fails the send rather than delivering it unquoted, which the
  description says.
* Mentions on Reply, Send Audio, Send Sticker and Send Template, which the
  server accepts and the node never sent.
* Link Preview on Send Text and Send Template, as a three-way choice rather
  than a boolean, because leaving it to the engine is meaningfully different
  from either explicit value and the two engines disagree about which way their
  default falls.
* Custom Link Preview on Send Text, rebuilt from its filled fields so an opened
  but empty collection is not sent as an object the server refuses. Pairing it
  with No Preview is the one combination the server rejects, so the node
  refuses that pair with a message naming both fields.
* Address on Send Location, which renders under the location name.

The quotable set and the mentionable set are deliberately separate: Send
Location and Send Contact can quote but cannot tag, and Send Template and Edit
can tag but cannot quote. Each field is gated by operation, because the server
rejects any body field the target DTO does not declare.
Chat gains Archive, Pin, Mute and Clear Messages. Clear Messages is the one
chat route that names its chat in the path rather than the body, and it is
distinct from Delete: it empties the conversation and keeps it.

Mute needed care. The server takes an absolute expiry in epoch milliseconds
with no duration form, and it distinguishes an omitted key from an explicit
null: omitting it is rejected, while null is how a chat is unmuted. The field
is a date picker, and leaving it empty sends null rather than dropping the key.
Archive and Pin both carry a flag with no server-side default, so it is always
sent. The pin description notes WhatsApp's three-chat cap, since a refused
fourth pin comes back as a success response carrying a false flag rather than
an error.

Contact gains List Blocked, which was missing while Block and Unblock shipped,
and addressbook Save and Delete. Save overwrites the whole entry, so the field
copy says that leaving Last Name empty clears a stored one; the key itself is
omitted rather than sent as an empty string, which would reach the engine as a
literal null.

Label gains Create or Update, Delete and Get Chats, so a workflow can build the
labels it attaches instead of depending on ones created by hand in the app.
Three things shaped the UI here:

* The label id is caller-chosen, so the write operations take a plain text
  field rather than the existing picker, which can only offer labels that
  already exist.
* Colour is an index from 0 to 19, not a hex value, and 0 is a real colour. It
  lives in a collection so that leaving it unset stays distinguishable from
  choosing the first colour.
* Reads and writes run on opposite engines, which a notice on the resource now
  states, since creating a label you cannot then list is otherwise baffling.

Add to Chat sent its label id without checking it was non-empty, so a blank one
became an opaque server rejection rather than a message naming the field.
…ration

The Trigger has offered a `group.join_request` event whose description points
at the membership-request routes, but the node exposed none of them, so a
workflow could be woken by a join request and had no way to act on it. Group
now has Get, Approve and Reject for that queue. An empty requester list means
"act on every pending request", which is why those two operations parse the
list directly instead of reusing the participant helper: that helper refuses an
empty list, which is exactly the meaningful case here.

Group also gains:

* Get Join Info, which previews a group from an invite code without joining. It
  accepts a full invite link and reduces it to the code, like Join already did.
* Get, Set and Delete Picture, reusing the existing media resolver so a picture
  can come from a binary field, a URL or base64 exactly as a profile picture can.
* memberAddMode in the settings patch, a field both engines support and Get
  Settings already returned.

Channel gains Create, Delete, Mute, Demote Admin and Transfer Ownership.

Delete and the existing Unsubscribe are kept deliberately far apart. Delete
destroys the channel for every subscriber and only its owner can do it;
Unsubscribe merely unfollows it for this account. The server put them on
different verbs and paths so a slip cannot turn one into the other, and the
node keeps that separation: distinct operations, no shared parameters, and a
notice on Delete saying what it does.

Two long-standing gaps in the channel copy are also closed. The Channel ID
field now says the picker is populated by a listing that only whatsapp-web.js
supports, so a Baileys user knows to supply the ID from an expression rather
than wondering why the list is empty. The message limit now carries the
hundred-item ceiling the server silently clamps to, instead of accepting a
larger number and quietly returning fewer.
…cture removal

The node has offered a "Send as Voice Note" toggle on Send Audio for several
releases, but nothing in the pipeline transcodes: the gateway forwards the bytes
it is given and labels them as Ogg/Opus because that is the default when no type
is supplied. Sending an MP3 with that toggle on therefore produced a microphone
bubble that would not play, and the declared type described bytes that were not
there.

A new Media resource closes that. Convert to Voice Note and Convert to Compatible
Video return base64 plus the type the bytes now are, which drops straight into
Send Audio or the new voice status. Conversion is optional on the server and
needs ffmpeg, so Check Availability reports whether the deployment has it, and
the field copy says a 503 here means conversion is off or busy rather than a bad
request. The convert body is built directly rather than through the shared media
resolver, because that resolver also emits a MIME type and this DTO refuses any
field it does not declare.

Also added:

* Status > Send Voice, which nests its media under `audio` the way the image and
  video statuses nest under theirs, with a notice pointing at the converter.
* Call > Create Link, which produces a shareable audio or video call link. Both
  DTO fields are required, so a blank start time means now rather than an omitted
  key. The response carries only the link, so the copy says the node cannot
  report an expiry.
* Profile > Delete Picture, which completes a resource that could set a picture
  but never remove one.

The Call handler previously refused every operation but Reject through an early
return; it is now a switch, and its comment no longer says the call link route is
uncovered.
Every subscribed event started a full workflow execution, even when the
workflow's first step was to discard it. The gateway has supported server-side
filters for some time, and the action node's Webhook Update already exposed
them, but neither Webhook Create nor the Trigger could set one.

The Trigger now takes Filters and registers them with the webhook, so a
non-matching event is dropped at the gateway. Filters are part of the
registration fingerprint, so editing one re-registers on the next activation
rather than leaving the stored filter in place. The field copy states the two
things that otherwise cost an afternoon: filters only narrow message events, so
session, group and call events arrive regardless, and a filtered-out delivery is
silent, which looks identical to nothing having happened.

Webhook Create gains the same Filters field plus Headers and Retry Count, all
three of which Update already had. Create and Update now share one JSON parser
rather than each carrying their own.

Also in this pass:

* Get Message Stats sends a Period. It was locked to the last 24 hours because
  the query was never sent, so the weekly and monthly windows were unreachable.
  This route binds its query to a schema, so the key is always sent.
* The audit Limit and the channel message Limit carry the ceilings the server
  silently clamps to, instead of accepting a larger number and quietly
  returning fewer.
* Audit Severity is a dropdown over the three values the server recognises.
  Action stays free text, since its vocabulary is long and grows, but its
  description now says an unrecognised value returns an empty page rather than
  an error, which previously made a typo indistinguishable from no activity.

The webhook lifecycle test harness ignored the default argument to
getNodeParameter, unlike n8n itself, so a parameter read with a fallback came
back undefined in tests only.
Two resources remained unreachable from the node.

Automation Rules are the gateway's own autoreply: a matching inbound message is
answered by the server, without a round trip through n8n, so an out-of-hours
acknowledgement still goes out when the workflow is not running. All five
routes are exposed. The cooldown is surfaced with its purpose stated, because
it is the guard against two auto-repliers answering each other indefinitely and
zero disables it. Match conditions reuse the webhook filter shape, which the
field copy points out along with the consequence: they only see message events.

The Catalog reads complete the product feature. Send Product shipped in this
release, but the product ID it needs had no source inside the node, so a user
had to find it elsewhere. Get, List Products and Get Product close that. Both
they and Send Product are Baileys only, which the resource notice states rather
than leaving a 501 to explain itself.

That brings the node to every session-scoped route the server publishes. What
is still deliberately absent is the server administration surface: the infra,
plugin, ingress and integration controllers, and the Prometheus endpoint, which
authenticates with its own token rather than the API key this credential holds.
The operation table listed 101 operations for a node that now offers 157, and
the compatibility section described a route surface that stopped at v0.10.9.

* The table is regenerated from the node description itself, so it lists every
  resource and operation the node actually offers and cannot drift again.
* The compatibility floor moves to 0.16.0. That is now set by the routes rather
  than the event catalog: Call > Create Link arrived in v0.16.0, above the
  0.15.0 the event list needs. The section names which release each part of the
  new surface came from, and adds a table of the four optional fields that need
  a server newer than the floor, since those return a 400 rather than degrading.
* The "not offered" note is rewritten. The catalog reads and Send Product are
  offered now, so what remains deliberately absent is the server administration
  surface. A new note collects the operations that exist on one engine only,
  including why the Channel ID dropdown cannot populate on Baileys.
* The Trigger section documents server-side filters, with the two caveats that
  matter: they narrow only message events, and a suppressed delivery is silent.

Five operation actions are reworded to satisfy the Creator Portal's sentence
case rule, which rejects an apostrophe or an internal capital, and the audit
Severity filter is a dropdown over the three values the server recognises
instead of a second free-text field duplicating the first.
Eight problems found while tracing the new code paths end to end.

**Webhook Test was swallowed by de-duplication.** A test delivery's idempotency
key is derived from the webhook id alone, so every test of the same webhook
carries an identical key. Keying de-duplication on that value meant only the
first test ever reached the workflow, and a manual probe is exactly the thing
that must always run. Test deliveries are now exempt.

**A Filters value supplied by expression crashed activation.** The field is
`type: 'json'`, so an expression-driven value arrives as an object rather than
text, and calling `.trim()` on it threw. The ordering made it worse: checkExists
had already deleted the working registration by then, leaving the trigger
unregistered and unable to reactivate. Both nodes now read the field through one
shared helper that accepts either shape.

**A filter attached out of band was invisible to the drift check.** `filters` is
serialized back by the server, and a filter added to the trigger's own
registration suppresses deliveries with no other trace, so it is now compared
alongside `active`, `url` and `events`. The comparison ignores key order, since
the value round-trips through a JSON column and a reordered but identical filter
must not force a re-registration. The config fingerprint normalises the same way,
so text and the equivalent object are one configuration.

Also:

* Media conversion asserts the binary property before reading it, so a wrong
  field name names the field instead of surfacing as an opaque API error.
* Session Update Config refuses a non-numeric attempt cap. `NaN < 0` is false, so
  it would have serialized to `null` and silently reset the cap to unlimited.
* The Filters help text drew the hazard line in the wrong place. Filters narrow
  only message events, but within that family an `is` condition on a field a
  given event does not carry suppresses it outright, so a sender filter alongside
  a Message Ack subscription drops every ack.
* Vote Poll now states it is whatsapp-web.js only, and that an option whose text
  contains a comma has to be given as a JSON array.
* Three descriptions corrected: the label ID field is shown for Delete and no
  longer describes only writing, an empty conditions object is refused rather
  than matching everything, and the comment about absent catalog reads no longer
  contradicts the Catalog resource added alongside it.
The route answers 404 for five distinct situations, and because the response is
raw bytes the server's own explanation never reaches the workflow. The most
surprising one is that media sent by URL is not retrievable at all, since the
gateway stores the URL rather than the bytes, while the same send made from a
binary field or base64 is.

Presence > Subscribe built its error message by interpolating the value it had
just rejected, so a malformed id produced an example that was not valid either.
The example is now fixed text, and the comment above it states which sibling
routes differ and why, rather than implying every other route accepts a bare
number.
@rmyndharis
rmyndharis merged commit 7286851 into main Aug 25, 2026
1 check passed
@rmyndharis
rmyndharis deleted the feat/sync-openwa-0.23 branch August 25, 2026 04:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant