Build WhatsApp workflows with n8n
Connect OpenWA to n8n, an open-source workflow automation tool, using the official community nodes. By the end of this page you'll have the nodes installed, an OpenWA credential configured, and a working auto-reply workflow: an incoming WhatsApp message triggers a reply.
- npm package:
@rmyndharis/n8n-nodes-openwa(current version1.0.1) - Repository: rmyndharis/OpenWA-n8n
The package ships two nodes:
| Node | Type | What it does |
|---|---|---|
| OpenWA | Action | Calls OpenWA operations as a workflow step — send messages, manage sessions, chats, contacts, groups, labels, templates, status, channels and webhooks, read a business catalog, convert media, drive presence, and manage autoreply rules |
| OpenWA Trigger | Trigger | Starts a workflow when a WhatsApp event arrives — messages, session, group, call, and presence events |
- A running OpenWA gateway that n8n can reach over the network. See Installation.
- A connected session that has finished pairing. See Your first session.
- An API key from the dashboard. See the Authentication guide.
- An n8n instance where you can install community nodes (self-hosted).
- An OpenWA server ≥ 0.16.0. The nodes are verified against v0.23.4.
Package 0.9.1 raised the required server from 0.10.9 to 0.14.0. Five of the events the
Trigger offers — session.restriction, presence.update, call.accepted, call.rejected, and
call.missed — do not exist in core before v0.14.0, and the gateway validates a webhook
registration against its own event list. A Trigger subscribing to any of them against an older
gateway is refused at registration with a 400, so the workflow fails to activate rather than
activating and quietly never firing.
Package 0.9.2 raised it again, to 0.15.0, by offering group.join_request, which core has
dispatched only since v0.15.0. Package 1.0.0 raises it once more, to 0.16.0, where the
calls/link route the Call resource needs arrived. That is the floor this guide describes.
Install the nodes
Install the community package on a self-hosted n8n instance.
From the n8n UI
- In n8n, open Settings → Community Nodes.
- Click Install.
- Enter
@rmyndharis/n8n-nodes-openwaand accept the community-node risk prompt. - Restart n8n.
Manual install
cd ~/.n8n/nodes
npm install @rmyndharis/n8n-nodes-openwa
Restart n8n after installing. Both nodes then appear in the node picker when you search for "OpenWA".
n8n Cloud only allows community nodes verified by n8n, and this package is not currently one of them. Use a self-hosted instance to run it.
Configure the OpenWA API credential
Both nodes share one credential. Create it once and reuse it across every OpenWA node in your instance.
| Field | Required | Default | Description |
|---|---|---|---|
| Server URL | Yes | http://localhost:2785 | Your OpenWA server URL, without a trailing slash or /api. Use HTTPS in production (for example https://wa.yourserver.com). |
| API Key | Yes | — | The API key from your OpenWA dashboard. The node sends it as the X-API-Key header on every request. |
When you click Test in the credential dialog, n8n makes an authenticated
GET /api/sessions request to your server. The test passes only if the key is valid, so a
wrong key or unreachable server fails immediately.
Create a dedicated API key for n8n rather than reusing an admin key, so you can revoke it without disrupting other clients. See the Authentication guide.
Most reads work with a plain VIEWER key. Writes — sends, group changes, profile changes, and webhook management — need an OPERATOR-role key (the default role). An ADMIN key is needed for every API Key operation except Validate, which any valid key may call, and for System → Get Settings, Get Audit Log, Get Stats Overview and Get Message Stats, plus Webhook → Get Delivery Failures. Several reads are not VIEWER-reachable despite returning nothing but data: System → Search, Session → Get QR, Template → List and Get, and Webhook → List, List All and Get all need OPERATOR, as does every Automation Rule operation, List and Get included. A VIEWER key is enough for the rest.
Some surfaces additionally refuse a session-scoped key whatever its role, because they carry no
session dimension for the scope check to apply to: the API-key management operations, System → Get
Settings, Get Stats Overview and Get Message Stats, and Session → Create and Update Proxy. Get Audit
Log is the exception among the ADMIN reads: a session-scoped key is accepted there and simply sees a
narrowed result. A 403 almost always means the key's role is too low or its scope too narrow, not
that the request was malformed.
You can confirm the same credential by hand with curl before building a workflow. This hits
the same send endpoint the Send Text operation uses, so a success here confirms both the
credential and a connected session:
curl -X POST http://localhost:2785/api/sessions/8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a/messages/send-text \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "628123456789@c.us",
"text": "Hello from OpenWA!"
}'
On success the gateway returns 201 Created with the message id and timestamp:
{
"messageId": "true_628123456789@c.us_3EB0123456789",
"timestamp": 1706868000
}
A 400 means the session id is unknown or the session was never started, or the body is invalid;
this route does not answer 404. A 409 means an engine exists but is not connected yet, so the
request never reached WhatsApp: wait for ready and retry. Replace the UUID with your own session
id — the id from the create response, not the name you gave it — and 628123456789@c.us with
a real recipient chat id.
The OpenWA node (actions)
The action node maps a Resource and Operation to one OpenWA call. Package 1.0.1 covers the
nineteen resources below, spanning the operations workflows reach for most often.
For field-level detail of every operation, see the
n8n Node Reference.
| Resource | Operations | What it covers |
|---|---|---|
| Session | Create, Start, Stop, Force Kill, Log Out, Delete, Get QR, Request Pairing Code, Get Status, List All, Get Stats Overview, Get / Update Config, Get / Update Proxy | Session lifecycle, pairing, and per-session configuration |
| Message | Send Text / Image / Video / Audio / Document / Sticker / Location / Contact / Poll / Template / Product, Reply, React, Edit, Delete, Forward, Pin, Unpin, Star, Vote Poll, Send Bulk, Get Batch Status, Cancel Batch, List, Get History, Get Reactions, Get Media | Everything that sends or acts on a message |
| Contact | Check Exists, Get Info, Get Phone, Get Profile Picture, Get Profile Pictures, List, List Blocked, Save, Delete, Block, Unblock | Contact lookup, the addressbook, and blocking |
| Chat | List, Mark Read, Mark Unread, Delete, Clear Messages, Archive, Pin, Mute, Set State | Conversations, their archive/pin/mute state, and typing or recording indicators |
| Group | List, Get, Create, Join, Get Join Info, Leave, Add / Remove / Promote / Demote Participants, Update Subject, Update Description, Get Settings, Update Settings, Get Invite Code, Revoke Invite Code, Get / Approve / Reject Membership Requests, Get / Set / Delete Picture | Group administration, the join-approval queue, and the group picture |
| Profile | Set Name, Set Status, Set Picture, Delete Picture | The session's own WhatsApp profile |
| Label | List, Get, Create or Update, Delete, Get Chats, Get for Chat, Add to Chat, Remove From Chat | WhatsApp Business labels |
| Status | List, Get by Contact, Get Media, Delete, Send Text, Send Image, Send Video, Send Voice | Reading and posting Status (Stories) |
| Template | List, Get, Create, Update, Delete | Stored templates with {{variable}} placeholders |
| Channel | List, Get, Get Messages, Create, Subscribe, Unsubscribe, Delete, Mute, Demote Admin, Transfer Ownership | WhatsApp Channels, both the ones the session follows and the ones it owns |
| Call | Reject, Create Link | Declining an incoming call, and sharing a call link |
| Observability | Check, Check Liveness, Check Readiness | Server health probes |
| System | Get Settings, Get Stats Overview, Get Message Stats, Get Session Stats, Get Audit Log, Search | Server-wide reporting and cross-session search |
| API Key | List, Get, Create, Update, Revoke, Delete, Validate | API-key administration |
| Webhook | Create, Get, List, List All, Update, Delete, Test, Get Delivery Failures | Webhook registration and delivery inspection |
| Automation Rule | List, Get, Create, Update, Delete | Server-side auto-reply rules |
| Catalog | Get, List Products, Get Product | WhatsApp Business product catalog (Baileys sessions only) |
| Media | Check, Convert Voice, Convert Video | Server-side conversion into the formats WhatsApp plays |
| Presence | Subscribe, Get, Set Own | Presence subscription, per-chat reads, and the account's own presence |
See the n8n Node Reference for the full field-level detail
of every operation. A few endpoints are deliberately absent because the server cannot serve
them: Send Catalog and PUT /api/settings were removed from the server in v0.19.0 (both always
answered 501); settings are environment-derived and read-only, so System exposes a read alone;
and /api/metrics authenticates with its own bearer token rather than the X-API-Key this
credential carries. The Catalog resource works only against a Baileys session, since
whatsapp-web.js answers 501 for every catalog call; sending a product from that catalog is
Message → Send Product, not a Catalog operation.
The node is not a full mirror of the API either. Four server areas have no resource at all: plugin management, the infrastructure routes, Integration Fabric instance management, and the Prometheus metrics scrape. All of them are reachable with n8n's HTTP Request node using the same credential. The complete endpoint list is in the API reference.
When you send an image or document from a Base64 source, also set the MIME Type field
(for example image/png or application/pdf) — OpenWA requires it for base64 payloads. The
Binary source fills this in from the binary metadata, and the URL source needs
nothing extra.
The OpenWA Trigger node (events)
The trigger node starts a workflow when a WhatsApp event occurs. When you activate the workflow, the node registers a webhook in OpenWA pointing at n8n's webhook URL; when you deactivate it, that webhook is removed. You never manage the webhook by hand.
Select one or more events to subscribe to. Package 1.0.1 offers all twenty-three
events the gateway dispatches:
| Event | Description |
|---|---|
message.received | New incoming message |
message.sent | Message successfully sent |
message.ack | Message delivery or read acknowledgement |
message.failed | Message failed to send |
message.revoked | Message deleted for everyone |
message.edited | Message text, or a media message's caption, edited |
message.reaction | Reaction added to or removed from a message |
status.received | A contact's Status (Story) is received |
session.status | Session status changed |
session.qr | QR code generated for scanning |
session.authenticated | Session authenticated |
session.disconnected | Session lost connection |
session.reconnect_loop | Session stuck in a reconnect loop (once per 5 consecutive attempts) |
session.restriction | WhatsApp places or lifts a restriction on the account |
presence.update | A subscribed chat's presence changes — Baileys only |
group.join | A participant joins a group the session belongs to |
group.leave | A participant leaves a group the session belongs to |
group.update | A group's metadata or roster changes |
group.join_request | Someone asks to join a group the session administers; fires only when the group has admin approval enabled |
call.received | An incoming WhatsApp call is detected |
call.accepted | An incoming call is answered — Baileys only |
call.rejected | An incoming call is declined, including by auto-reject — Baileys only |
call.missed | An incoming call goes unanswered — Baileys only |
Every event above is available on any server this package supports, so there are no per-event
version caveats left to track — the ≥ 0.16.0 floor covers them all.
group.join_request reached the package late: the gateway has dispatched it since core v0.15.0,
but no node offered it until package 0.9.2. On 0.9.1 neither the Trigger nor the action node's
Webhook → Create and Update operations can select it, because all three read one shared list. On
0.9.1, register that subscription by hand with n8n's HTTP Request node against
POST /api/sessions/{sessionId}/webhooks, pointing it at an n8n Webhook node's URL, or upgrade
the package.
The three call-outcome events fire on Baileys only. whatsapp-web.js hooks the call
collection's insert and sees no status at all, so it can report the ring but never how the call
ended — a workflow triggered on call.missed simply never runs on a whatsapp-web.js session.
call.received fires on both engines.
presence.update is also Baileys-only, and it stays silent until you subscribe the chat with
POST /api/sessions/{sessionId}/presence/subscribe. whatsapp-web.js answers that route with
501.
Signature verification
The trigger has an optional Webhook Secret. When set, the secret is registered with
OpenWA, and OpenWA signs every delivery with HMAC-SHA256 in the
X-OpenWA-Signature: sha256=<hex> header. The node verifies each delivery against the raw
request body and rejects any that fail with HTTP 401. Leave it empty to skip verification.
Changing or clearing the secret takes effect on the next activation. Deactivate and reactivate the workflow to re-register it.
Trigger payload
The trigger emits one item per event. The event is wrapped in an envelope; the actual event
data lives under data:
{
"event": "message.received",
"timestamp": "2024-01-15T10:30:00Z",
"sessionId": "default",
"idempotencyKey": "a1b2c3d4e5f6",
"deliveryId": "9f8e7d6c5b4a",
"data": {
"id": "3EB0F5A2B4C1",
"chatId": "628123456789@c.us",
"from": "628123456789@c.us",
"body": "Hello!",
"type": "text",
"timestamp": 1705312200
}
}
Reference fields in downstream nodes with n8n expressions — for example {{$json.data.body}}
for the message text and {{$json.data.chatId}} for the chat to reply to.
A few notes from the payload contract:
- Read message fields from
data(data.body,data.chatId), and the message identifier fromdata.id— incoming payloads useid, notmessageId. typeis engine-neutral: voice notes arevoice, shared contacts arecontact, and plain chats aretext.- Check Exists returns
whatsappId, the engine-canonical chat id, which may differ from the number you sent (for example an@lidid).
OpenWA guarantees at-least-once delivery: it retries a failed POST and replays anything a gateway
crash stranded. Both carry the same idempotencyKey, so key your dedup step on that, not on
deliveryId, which identifies one attempt and is re-minted on every replay. Use the Trigger's own
Deduplicate Deliveries toggle, or a Remove Duplicates node on idempotencyKey.
Example: auto-reply to incoming messages
This is the smallest useful workflow — trigger on an incoming message, check the text, and reply.
- OpenWA Trigger — select your OpenWA API credential and subscribe to
message.received. - IF — condition:
{{$json.data.body}}containshello(case-insensitive). - OpenWA → Send Text — on the true branch, configure:
- Session Name or ID: pick your session from the dropdown, which lists the sessions on your server
- Chat ID:
{{$json.data.chatId}} - Message:
Hi! Thanks for reaching out — how can we help?
- Activate the workflow. The trigger registers its webhook with OpenWA automatically.
Send "hello" to your WhatsApp number, and you'll get the reply within seconds.
From here, swap the IF for a router to branch on keywords, append each new lead to Google
Sheets before replying, or post a Slack alert when session.disconnected fires. The trigger
gives you the inbound event; the action node sends the response.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Credential Test fails | Server unreachable, wrong key, or a trailing slash in Server URL | Confirm the server is running and reachable from n8n, the key is correct, and Server URL has no trailing / or /api. |
| Trigger fires once in the editor, then goes quiet | Listen for test event registers n8n's test URL, which stops after a single delivery | Activate the workflow so the trigger registers its production URL. |
| Trigger never fires | OpenWA can't reach n8n's webhook URL | Confirm n8n's webhook URL is reachable from the gateway (check firewalls and proxies) and that the session is connected. Then ask the gateway which side dropped the event with GET /api/webhooks/delivery-failures?sessionId={sessionId} (ADMIN key): a row means OpenWA delivered and n8n rejected it, an empty list means the event never reached delivery. |
A call.accepted, call.rejected, or call.missed trigger never runs | The session runs whatsapp-web.js | Those three outcomes are Baileys-only. Use call.received, which fires on both engines, or move the session to Baileys. |
Workflow won't activate; webhook creation fails with 400 | The gateway doesn't know one of the selected events | session.restriction, presence.update, and the three call outcomes need OpenWA ≥ 0.14.0, and group.join_request needs ≥ 0.15.0. Upgrade the gateway, or deselect the events it doesn't know. |
Send Text returns 400 | Unknown session id, session never started, or an invalid chat id | Pick a real session from the node's Session Name or ID dropdown, start it, and check the chat id format (628123456789@c.us). |
Send Text returns 409 | An engine exists but is not connected, so the request never reached WhatsApp | Wait for the session to reach ready, then retry. |
| Message sends but never arrives | Recipient isn't on WhatsApp | Use the Check Exists operation before sending to unverified numbers. |
Next steps
- n8n Node Reference — every operation and field in detail
- Webhooks — the event model behind the trigger node
- Sending messages — every send operation and its fields
- API reference — every endpoint the action node can call
- n8n Community Nodes documentation — installing and managing community nodes