Package Information
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 toDEFAULT_BASE_URLwhen 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 thePOST /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 withoutGET /api/instancesstill works by pasting an ID.
The dropdown cannot colour an entry red: n8n's option
descriptionis 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 sinceGET /api/instanceswas 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 stringstatus@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 isDEFAULT 0in RexWapi
(config/migrations.ts:134-137), so the node preselects onlymessage, 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
wherefromMeis false (connection.ts:819), so they are delivery/read
tracking for your own outbound — not the contact reading their messages to you. webhook_activeis 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_urlcolumn, 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 empty200is 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-messageandsend-react
validatetoas 8–20 characters, which a group JID cannot satisfy. The media
routes only check thattois present, so a group JID does work there. The node
detects the mismatch and says which operation to use instead. senderNameis always empty on inbound message webhooks. RexWapi passessenderNamebut readspushNamewhen building the payload, and the two keys do
not match. Key onfrominstead.- 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 sendsmessageId 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 carryingsuccess: false is treated as a failure. It touches no network and never invokes a
send.
Notes
n8n-workflowis a peer dependency — n8n provides it at runtime, and
bundling a second copy is how a node ends up with two incompatible copies.form-datais 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 andlast_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.