n8n-nodes-walayer
v0.1.0
Published
n8n community node for WALayer — send WhatsApp messages, manage sessions and contacts, and trigger on verified WALayer webhooks
Maintainers
Readme
n8n-nodes-walayer
An n8n community node package for WALayer — send WhatsApp messages, manage sessions and contacts, and start workflows from signature-verified WALayer webhooks.
Two nodes ship in this package:
| Node | Purpose | |---|---| | WALayer | Action node — send, message lookup, session list/get/create, contact exists/block/unblock | | WALayer Trigger | Trigger node — subscribes a WALayer webhook endpoint and starts the workflow on each verified delivery |
Install
From the n8n UI (self-hosted, community nodes enabled):
Settings → Community nodes → Install → n8n-nodes-walayer.
Manually, into your n8n user folder:
cd ~/.n8n/nodes
npm install n8n-nodes-walayerRestart n8n. Both nodes appear under WALayer.
Credential
Create a WALayer API credential:
| Field | Value |
|---|---|
| API Key | a key minted in the WALayer dashboard (wsk_live_… / wsk_test_…) |
| Base URL | https://api.walayer.com unless you run a self-hosted deployment |
The key is stored encrypted by n8n, masked in the UI, and injected into the Authorization header
at request time. It is never a node parameter, never part of an execution record, and nothing in
this package reads it.
Scopes the credential needs, per operation: messages:write (send), messages:read (message get),
sessions:read / sessions:write, contacts:read / contacts:write, webhooks:write (the
trigger subscribes and unsubscribes for you).
Operations → routes
Every operation maps to a documented route in docs/04-api-spec.md. Nothing is invented.
| Resource / operation | Route |
|---|---|
| Message → Send | POST /v1/sessions/:id/messages (§5) |
| Message → Get | GET /v1/messages/:id (§5.6) |
| Session → List | GET /v1/sessions (§3.1) |
| Session → Get | GET /v1/sessions/:id (§4.3) |
| Session → Create | POST /v1/sessions (§4.1) |
| Contact → Exists on WhatsApp | GET /v1/contacts/:phone/exists (§3.3) |
| Contact → Block / Unblock | POST /v1/sessions/:id/contacts/:jid/block | /unblock (§3.3) |
| Trigger → subscribe / unsubscribe | POST /v1/webhooks, GET /v1/webhooks, DELETE /v1/webhooks/:id (§3.5) |
Send supports text, image, video, audio (with voice-note flag), document, sticker,
location, contact card and reaction. Media travels by reference — a med_… id from
POST /v1/media, or a public HTTPS URL that WALayer fetches server-side. The node never uploads
bytes.
Idempotency — read this before you build a retry
Every send carries an Idempotency-Key, which becomes the message's client_msg_id under a unique
constraint. Leave the option empty and the node derives a key from the workflow id, the execution
id, the node name and the item index. That is stable across n8n's Retry on fail, so a retried
execution replays the original message rather than sending a real person the same WhatsApp message
twice.
Set the key explicitly when the same logical message can be produced by different executions — use the business identity of the message (order number, ticket id):
Options → Idempotency Key → {{ "order-" + $json.order_id }}A blind resend is not a retry. If a send comes back ACK_TIMEOUT or SEND_UNKNOWN, the outcome is
genuinely unknown and WALayer is reconciling it — check GET /v1/messages/:id before doing
anything. The node marks those errors as non-retryable for exactly this reason.
Errors
WALayer error codes surface as themselves. A paced send shows
WALayer 429 RATE_LIMIT_PACING (warmup stage 2/4, daily cap 180, retry after 45s)with the explanation "Your WhatsApp number's pacing budget is exhausted…", not "the service
returned an error". The two rate-limit concepts stay distinct: RATE_LIMIT_API protects the
platform, RATE_LIMIT_PACING and WARMUP_CAP_EXCEEDED protect the customer's number.
SESSION_SUSPENDED, SESSION_BANNED and RECIPIENT_SUPPRESSED read as themselves too, and none
of them is retryable.
Trigger: how deliveries are verified
On activation the trigger calls POST /v1/webhooks with the node's webhook URL and the events you
selected, and stores the returned signing secret in the node's static data — never in a parameter,
because parameters are visible in the editor and copied into every execution record.
Each delivery is then checked before the workflow runs:
- the signature is verified against the raw body with
verifyWebhookfrom@walayer/sdk(X-Signature: v1,sha256=HMAC-SHA256(secret, "{timestamp}.{rawBody}")), in constant time; X-Timestampmust be inside the 5-minute replay window;X-Delivery-Idis deduplicated — WALayer delivers at-least-once;- anything that fails is answered
401and the workflow does not start.
A missing secret fails closed: the trigger refuses the delivery rather than accepting unsigned traffic. If that happens, deactivate and reactivate the workflow so it re-subscribes and receives a fresh secret (the secret is issued exactly once, at creation).
The signing secret lives in n8n's workflow static data. Treat a workflow export as sensitive.
Build and test
pnpm install # installs n8n-workflow (peer/dev) and the two WALayer packages
pnpm run build # tsc → dist/, then copies the node icon
pnpm run typecheck
pnpm test # node --test: package-contract checks (see below)The unit tests for request building, error mapping and signature verification live in
integrations/shared and run with no dependencies at all. The tests in this package are static
checks on the things that break in the wild: the n8n block pointing at files that will exist
after a build, no credential-shaped node parameter, no logging, rawBody: true on the trigger, and
an always-derived idempotency key.
Publish
pnpm run build
npm publish --access publicn8n's community-node verification additionally requires the package to pass
eslint-plugin-n8n-nodes-base. That linter is not installed in this repository (see the note in
integrations/README.md).
