rexwapi

RexWapi nodes for n8n: a webhook trigger for inbound WhatsApp messages, status updates, reactions, delivery updates and read receipts, plus a node to send text, image, audio, video and files, reply and react.

Package Information

Released: this month
Downloads: 0 weekly / 0 monthly
Latest Version: 1.9.0
Author: Mahadie Reza

Documentation

n8n-nodes-rexwapi

n8n nodes for RexWapi — send and receive WhatsApp
messages from an n8n workflow.

Two nodes, one credential:

Node Shape on the canvas What it is
RexWapi Trigger trigger Starts the workflow on a WhatsApp message, status update, reaction, delivery update or read receipt. Two fields: Events, Path, plus an Authentication dropdown that is off by default.
RexWapi rounded square Sends text, image, audio, video and files; replies; reacts; checks numbers; reads instance info.

Install

npm install n8n-nodes-rexwapi

Or, from this directory, to link it into a local n8n:

npm install
npm run build
npm link
# in your n8n install:
npm link n8n-nodes-rexwapi

Credentials

One credential type, RexWapi API, with one field:

Field Notes
API Key Your RexWapi API key from Settings → API Key in the dashboard. Sent as x-api-key.

That is the whole form. There is no Base URL and no Test button:

  • RexWapi is one hosted address for every user, so asking for it only creates a way
    to get it wrong. The node falls back to DEFAULT_BASE_URL when the credential
    carries none.
  • A Test button would be decoration. The only API-key route that proves anything is
    GET /api/instances, which a self-hosted server may predate — so the button
    would be red on a working setup and would block saving. A wrong key fails on the
    first send instead, carrying the server's own message.

Use the account key, format rexwapi_<64 hex>. The key returned in the
POST /instances response body is a different value and is never accepted — it is
not stored anywhere and apiKeyMiddleware does not read it. If a send fails with
401 and your key looks right, this is almost always why.

Panel width

Both nodes set parameterPane: 'wide'. The field descriptions are long on purpose —
the media-source notes and the event descriptions are what stop someone picking
base64 for a 5 MB video, or waiting for a receipt event the instance was never
switched on for — and in the default-width pane they push the Instance dropdown off
the right edge. The pane is still draggable; this just means it doesn't have to be
widened by hand first.

Instance

Both nodes take an Instance field, a dropdown of the instances your API key
owns. Each entry is named by its instance ID, with the paired number underneath:

rexlap_shop          +12025550123 · connected
rexlap_support       No account connected · scan a QR code to pair one
rexlap_old           No account connected

The value is the instance ID, because that is the URL segment every send needs
and the field the trigger emits as instanceId.

Two things this field deliberately is not:

  • Not locked. An unpaired instance is shown and still selectable — the fix is
    in the RexWapi dashboard, and disabling the choice would strand you.
  • Not the only way in. It stays typeable and expression-capable, so
    {{ $json.instanceId }} works from a trigger, and a self-hosted server without
    GET /api/instances still works by pasting an ID.

The dropdown cannot colour an entry red: n8n's option description is plain
secondary text with no styling. Naming the state beats a colour anyway —
connecting… tells you to wait, where red would only say something is wrong.

If the list comes up empty, the field now says why. The usual cause is a
RexWapi server that has not been redeployed since GET /api/instances was added,
which answers 404 — and the message names that specifically. A 401 or 402 is
reported as the key or plan problem it is. Either way you can still paste an
instance ID or use {{ $json.instanceId }}.

The RexWapi Trigger

Events

RexWapi POSTs five different event values, and they are not variations of one
shape:

event Emitted when Shape
message any inbound message flat envelope + messages[], may be batched
status_update a contact posted to their Status one message spread across the top level, type under type
reaction a reaction added or removed flat, singular; emoji: '' means removed
delivery_update an outbound message was delivered messages[].messages[], grouped by recipient
read_receipt an outbound message was opened same nesting, readAt instead of deliveredAt

Note that status_update has nothing to do with the two receipt events. It is
someone posting to their WhatsApp Status — the stories feature — which arrives
on the status@broadcast channel. from on that event is the literal string
status@broadcast, not a number.

Three things worth knowing before you build:

  • Each event is switched on in TWO places — here on the node, and on the
    instance in RexWapi. Both are required; enabled in only one, it never arrives.
    Every optional event column is DEFAULT 0 in RexWapi
    (config/migrations.ts:134-137), so the node preselects only message, which
    matches the server's own default rather than advertising four events that cannot
    arrive yet.
  • The two receipt events only cover messages you sent. RexWapi skips anything
    where fromMe is false (connection.ts:819), so they are delivery/read
    tracking for your own outbound — not the contact reading their messages to you.
  • webhook_active is a master switch, also off by default. Nothing at all
    arrives until it is on for the instance.

The webhook path

The Path field is used exactly as typed — no UUID is prepended:

https://run.example.com/webhook/rexwapi
https://run.example.com/webhook-test/rexwapi        (while testing)

This needs isFullPath: true on the webhook descriptor, because getNodeWebhookPath
(node-helpers.ts:1188) returns the path verbatim only under that flag; otherwise it
builds ${webhookId}/${path} and the URL reads
/webhook/1ba4ebc1-612a-4bc5-bbb9-eda7186cb9c8/rexwapi.

The trade: a full path is registered verbatim, so it must be unique on the n8n
instance. Two workflows using the same path collide and only one receives events.
Give each a distinct path — rexwapi-orders, rexwapi-receipts.

Note the constraint this puts on RexWapi itself: an instance has a single
webhook_url column, so one instance feeds one URL. Two separate n8n workflows
from one WhatsApp number needs either two instances or one dispatcher workflow.

Output

The body is passed through exactly as RexWapi sent it — one item per webhook
call, batched messages intact under messages[]. There is no reshape option,
because the payload is the contract with RexWapi and a second dialect would hide
any field the server adds later.

So event and instanceId are always at the top level of an item.
event is what a Switch node routes on; instanceId is what the send node's
Instance field takes as {{ $json.instanceId }}.

One event, one execution

There is nothing to respond with, and nothing to configure. The trigger answers
with a bare 200 and an empty body, then the workflow runs. No Response Mode, no
"Respond Using", no response code — the panel has exactly two fields, Events and
Path.

A webhook call always produces exactly one execution. RexWapi treats any
non-2xx as "the endpoint is down" and redelivers the same event up to three
times
with exponential backoff, and it abandons the request after 10 seconds.
So the trigger acknowledges the moment the webhook arrives, before the workflow
starts, and the status is settled there and cannot change afterwards. It cannot be
turned into a 500 by a slow node, an erroring node, a node that outputs nothing, or
a workflow that takes longer than 10 seconds.

A trigger that sent nothing would be worse, not better. The socket would hang
until RexWapi's 10-second timeout aborted it — which is a failure, and a failure
is exactly what triggers the redelivery. An empty 200 is the closest thing to
"no response" that is safe against this client.

The WhatsApp reply is therefore sent by a RexWapi node inside the workflow.
RexWapi does not read the response body at all — it checks the status and discards
the rest — so acknowledging early gives nothing up. Respond to Webhook is not an
alternative either: it accepts only a fixed list of built-in start-node types and
rejects a community trigger outright.

Filters

  • Events — only the selected events run.

A filter never refuses a request. An unwanted event is acknowledged with a 200
and no execution is created — there is nothing to see in the executions list,
because nothing ran. An event the node does not recognise is passed through to the
workflow rather than dropped, so a new server-side event shows up instead of
vanishing.

There is no Instance filter and never should be: a RexWapi instance has a single
webhook_url, so only one instance can point at a given URL anyway.

Authentication

One dropdown, default None:

Choice Behaviour
None Any request that reaches the URL runs the workflow.
Header Auth Reveals a Credential field. The named header must be present and match, or the request is answered 401 and nothing runs.

RexWapi instance webhooks carry no signature, so a static header is the only check
available — which is why None is the default. With it, a half-configured
instance cannot lock itself out: no header configured means no failures.

Choosing Header Auth also means RexWapi must be configured to send the same
header (webhook_header_name / webhook_header_value on the instance), and the
two must match exactly. A mismatch answers 401, and nothing runs — so if messages
stop arriving, check that pairing first.

Three behaviours worth knowing:

  • The credential is only read when you pick Header Auth. Switching back to
    None disarms the check immediately, even with a credential still saved. (That
    reversibility is the reason for the two-field shape — see below.)
  • Header Auth with no credential chosen yet still runs, rather than rejecting
    every delivery. An empty credential is treated as unconfigured, never as a header
    that must be present and blank.
  • A 401 produces no execution at all, so refused deliveries leave nothing in
    the executions list. RexWapi still retries a 401 three times, but those retries
    run nothing.

The RexWapi node

Operation Endpoint Notes
Send Text Message POST /api/:instanceId/send-message to, message
Reply to Message POST /api/:instanceId/send-message same, with replyToMessageId
Send Image POST /api/:instanceId/send-image caption, fileName
Send Audio POST /api/:instanceId/send-audio no caption — the route discards it
Send Video POST /api/:instanceId/send-video caption, fileName
Send File POST /api/:instanceId/send-file any document
React to Message POST /api/:instanceId/send-react messageId + emoji
Check WhatsApp Number POST /api/:instanceId/check-number exists is an answer, not an error
Get Instance Info GET /api/:instanceId/instance-info status, phoneNumber

Every send also accepts Typing Indicator (0–30 s, default 5, 0 or empty for none)
and Reply To Message ID.

Read receipts and the typing indicator

Both live on the send itself, so answering a message costs one HTTP request rather
than three. RexWapi marks the messages read, waits, shows the typing indicator, then
sends — the order a person produces.

Turn on Send Read Receipt and list the incoming message IDs, separated by commas:

ABC, DEF, GHI

A field accepts an expression too, so the usual case — marking the whole batch read —
needs no manual copying:

{{ $json.messages.map(m => m.messageId).join(", ") }}

The field also takes a real array ({{ $json.messages.map(m => m.messageId) }}) or a
JSON array as text, and trims and de-duplicates whatever it gets. Up to 20 IDs, one
round-trip however many you list.

This is deliberately not the same as the instance-level "mark as read" setting,
which ticks every message the instant it arrives whether or not anyone replied. That is
right for a human-operated line. This field is for an automation that has decided to
answer and wants the tick to happen only then.

Media sources

Three sources, and the choice is a real trade-off rather than three ways of
spelling one thing:

Source Transport Practical ceiling Use when
From a URL (default) JSON, fileUrl the full 16 MB forwarding a fileUrl from the trigger, or hosting the file anywhere public
From Binary Data multipart, field file the full 16 MB the bytes are already in the item
From Base64 JSON, <type>Base64 ~380 KB small files only

The base64 ceiling is the surprising one. The media cap is 16 MB but the JSON body
parser is capped at 512 KB, and base64 inflates by 4/3 — so base64 can never
reach the media limit. The node checks the estimated size before sending and
tells you to use a URL or binary instead, rather than letting a generic parse
error surface.

Forwarding incoming media

A trigger fileUrl is a plain string, so forwarding one needs no transformation:

Instance:  {{ $json.instanceId }}
To:        {{ $json.from }}
Operation: Send Image
Media:     From a URL
Media URL: {{ $json.fileUrl }}

Media URLs are signed and need no API key to fetch. They do expire.

Known limitations

These are properties of the RexWapi API as it stands, not gaps in the node. The
node is explicit about each one rather than offering a control that silently does
nothing.

  • No sticker, poll, contact, location, forward, delete or edit. None of those
    routes exist on the server, so they are not offered here — a node written from a
    generic WhatsApp-API spec would offer them and then 404.
  • Text and reactions cannot target a group. send-message and send-react
    validate to as 8–20 characters, which a group JID cannot satisfy. The media
    routes only check that to is present, so a group JID does work there. The node
    detects the mismatch and says which operation to use instead.
  • senderName is always empty on inbound message webhooks. RexWapi passes
    senderName but reads pushName when building the payload, and the two keys do
    not match. Key on from instead.
  • Instance and webhook setup is not in this package. Creating an instance,
    pairing a WhatsApp account (QR) and pointing an instance at your n8n webhook URL
    all live behind a JWT-protected API and are done in the RexWapi dashboard. The
    node is the n8n half of that pair, not the whole of it.

Development

npm install
npm run build      # tsc + copy the icons into dist/
npm run typecheck  # tsc --noEmit
npm run verify     # static checks on dist/, then the runtime checks

tsc only emits .js/.d.ts/.map, so scripts/copy-assets.mjs copies the
.svg icons into dist/. Without it every icon would point at a file that does
not exist and n8n would render a broken image with no error.

npm run verify runs two scripts, because the failures here are silent ones.

verify-package.mjs reads the built files and checks what n8n does not
complain about: a node path that does not resolve, an icon not beside the module
declaring it, a parameter shown but never read, an operation in the dropdown with
no handler.

verify-runtime.mjs then runs the compiled nodes against a stand-in execution
context and asserts the wiring that only exists at call time — that the trigger
really drops an unselected event instead of erroring, that Simple mode really
splits a batch, that a read receipt really exposes its to, that a reaction sends
messageId and not replyToMessageId, that audio drops its caption, that a group
JID is refused on text and accepted on media, and that a 200 OK carrying
success: false is treated as a failure. It touches no network and never invokes a
send.

Notes

  • n8n-workflow is a peer dependency — n8n provides it at runtime, and
    bundling a second copy is how a node ends up with two incompatible copies.
  • form-data is a real dependency: the binary media path is multipart, and a
    plain object body would be serialised as JSON with no file in it.
  • Do not set the trigger's Response Mode to "From Last Node". It is fixed at
    Immediately on the descriptor, and it must stay there. Under From Last Node
    n8n holds the HTTP response until the workflow finishes, and answers 500 when the
    workflow errors, when the last node outputs nothing, or on any post-execute
    failure — and every one of those 500s is three executions, because RexWapi
    redelivers on any non-2xx. A workflow slower than RexWapi's 10-second timeout is
    aborted client-side and delivered three times for the same reason.
  • Respond to Webhook will not work with this trigger. That node hard-codes the
    start-node types it accepts, and a community trigger is not one of them — it
    fails with "No Webhook node found in the workflow". Send the reply with a RexWapi
    node instead, which is what actually reaches the contact.

Publishing

The package becomes installable by name as soon as it is on npm.

npm login
npm run build
npm run verify         # all checks must pass before you publish
npm publish

Then on any self-hosted instance: Settings → Community Nodes → Install,
enter n8n-nodes-rexwapi — or n8n-nodes-rexwapi@<version> to pin one — accept
the risk notice, and install. Only Owner/Admin users can install community nodes.
n8n Cloud does not allow unverified community nodes at all.

npm version patch      # rewrites package.json and tags a commit
npm publish --otp=123456

The registry is append-only, so the version bump is what makes a new release.

The one change this package needs on RexWapi

GET /api/instances (src/modules/api/index.ts) — a read-only, API-key route
returning the caller's instances with their paired number and connection state.
It is what lets the Instance dropdown populate.

Every other /api route is scoped to one :instanceId, so without it an API-key
client can only act on an id it already knows and has to be handed that id by hand.
The route reuses the apiKeyMiddleware already applied at the router level and
selects only rexwapi_instance_id, phone_number, connection_status and
last_connected — no settings, no secrets, no usage figures.

The node degrades gracefully without it: the dropdown falls back to "type your
instance ID", and because the field is allowArbitraryValues and
expression-capable, nothing stops working.

Discussion