WhatsApp, as an n8n node.
Trigger a workflow on an inbound message, send text, media, templates and interactive messages as actions, and reach the thousand other apps n8n already speaks to. On every plan, including Free.
Installing
Two nodes in one package — an action node and a trigger node — installed the way n8n installs any community node.
# install from npm
n8n-nodes-cosend The action node is marked usable as a tool, so an n8n AI agent can call it directly rather than needing a wrapper workflow around it.
The trigger, and the two events worth installing for
Webhook, not polling, and that is a decision rather than a convenience. n8n's polling granularity bottoms out around a minute, and a thirty-second average reply delay is a product failure for a chat product. Polling every active workflow would also mean roughly 1,440 requests a day each, mostly returning nothing, against a product whose core tier is free. And status transitions are transient: a cursor-based poll coalesces them, a webhook delivers each one.
Filtering happens on our side, so a workflow subscribed to inbound messages never sees status noise.
| Event | Fires when |
|---|---|
| message.received | An inbound message — text, media, reaction or reply |
| message.status | sent · delivered · read · failed |
| message.echo | The owner replied from the WhatsApp Business app. It exists only on coexistence numbers, which is why no cloud-API-only vendor can emit it — and it is what lets a workflow cancel its own follow-up when a human has already answered Coexistence only |
| conversation.created | A new thread opened |
| conversation.updated | State, assignee, control or labels changed |
| conversation.window_expiring | The 24-hour service window closes in one hour. This turns the most common WhatsApp integration bug — sending into a closed window — into something you can build a workflow on |
| contact.consent_changed | An opt-in or an opt-out was recorded |
| automation.run_finished | A run reached a terminal state. Also run_failed |
| connection.disconnected | The WhatsApp connection broke. Also needs_reauth |
| usage.threshold_reached | 80% or 100% of a meter |
Every delivery is signed and the node verifies it before your workflow sees an item, rejecting anything stale. The webhook is created, checked and deleted for you as you activate and deactivate the workflow — you never paste a URL anywhere.
The honest limitation. A self-hosted n8n behind NAT with no public URL cannot receive webhooks from anyone, us included. n8n Cloud and any tunnelled or reverse-proxied instance work as they are; for a locked-down one, point a Cosend outbound webhook at n8n's built-in Webhook node instead and use the action node for the sending half.
The action node
Five resources. The parameter descriptions are generated from the same schemas the API validates against, so a field cannot be named one thing in n8n and another on the API.
| Resource | Operations | Worth knowing |
|---|---|---|
| Message | Send · Get · Get Many · Mark as Read · React | Send takes a message type sub-selector: text, media, template or interactive. Media comes from a binary field, a URL or a media ID. |
| Contact | Create · Get · Get Many · Update · Upsert · Delete · Add Tags · Remove Tags | Upsert looks up by phone number and then writes, so a workflow does not need an IF node in front of it. |
| Conversation | Get · Get Many · Update · Set Control · Mark Read · Add Note | There is no Create and no Close. A conversation exists because a message exists, and closing is Update with a state of resolved. |
| Automation | Trigger · Get Many · Get Run · Get Run Events | Get Run Events is the per-step trace, so a failing automation is debuggable from inside n8n. |
| Usage | Get · Get Breakdown | So a workflow can react to its own consumption — post to Slack when AI conversations pass 80% of what is included. |
One dropdown value is deliberately missing. Conversation control can be set to bot, pending human or human — but not to app, which is the coexistence state meaning the owner is replying from their phone. That state is observed, never set, and offering it would be offering you a guaranteed error.
Credentials
One credential type, an API key, and the choice of which key is the interesting part:
- An organization key for a workflow that touches several numbers.
- A connection key when the workflow is about one number — because it cannot reach any other one. Least privilege, by default, without configuring anything.
The base URL is a credential field rather than a constant, so EU-residency and staging users can repoint the node. n8n bans environment variables inside a community node, and this is the supported way to make a host configurable.
Saving the credential runs a real request against our identity endpoint, which is guaranteed never to fail for authorization reasons if the credential resolves at all — so you get a green tick before you build anything.
How the node is built, and why that is on a marketing page
n8n's requirements for a community node are strict, public, and mostly invisible from the outside. They are here because they are also the reasons this node cannot quietly become a liability in your workflow:
- Zero runtime dependencies
- The package declares none. It speaks raw HTTP through n8n's own helpers rather than bundling a Cosend SDK, which means nothing arrives in your n8n instance except the node itself.
- MIT, and the source is the published source
- Releases carry npm provenance from a public GitHub Actions workflow, so the package on npm is attestably built from the commit it names — and anyone, including n8n's own scanner, can re-download that commit and check it.
- Declarative, with two exceptions
- Most of the node is JSON rather than code, which is what n8n recommends and what keeps the surface small. Two operations need more than one request — sending media from a binary field, and upserting a contact — and only those two are written as code.