WATI Pro × n8n
Receive every incoming WhatsApp message in n8n via your per-tenant signed webhook, then send replies — text or media — through the WATI Pro REST API.
Get your tenant webhook secret + API key
In WATI Pro open Settings → Webhooks & API and copy:
- Incoming webhook secret — used to verify HMAC signatures from WATI.
- API key (starts with
wati_) — Bearer token for outbound REST calls.
In n8n, save both as environment variables: WATI_WEBHOOK_SECRET and WATI_API_KEY.
Create the n8n Webhook node
Add a Webhook node (Method: POST, Path: wati-incoming, Response: "Using 'Respond to Webhook' node"). Enable raw body in node options so the HMAC matches byte-for-byte.
Copy the n8n production URL and paste it into WATI Pro → Settings → Incoming webhook URL. Must be HTTPS.
Sample payload WATI Pro POSTs to your webhook:
{
"from": "+15551234567",
"text": "PRICE",
"messageId": "true_15551234567@s.whatsapp.net_3EB0...",
"deviceId": "8f3c…-uuid",
"receivedAt": "2026-06-28T11:32:01.245Z"
}Signature header: x-webhook-signature: sha256=<hex> over the raw request body using HMAC-SHA256 with your secret.
Verify the HMAC signature (Function node)
Reject any request whose signature doesn't match — anyone with the URL could otherwise spoof messages.
// n8n Function node — verify x-webhook-signature
const crypto = require('crypto');
const secret = $env.WATI_WEBHOOK_SECRET;
const sigHeader = $json.headers['x-webhook-signature'] || '';
const raw = JSON.stringify($json.body); // or use the rawBody buffer
const expected = crypto.createHmac('sha256', secret).update(raw).digest('hex');
const got = sigHeader.replace(/^sha256=/, '');
const ok = got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got,'hex'), Buffer.from(expected,'hex'));
if (!ok) throw new Error('Invalid WATI webhook signature');
return [{ json: $json.body }];Reply with text — POST /api/public/v1/send
Use the HTTP Request node. Phone number in E.164 (with country code). The response includes the messageId.
curl -X POST https://wati.pro/api/public/v1/send \
-H "Authorization: Bearer $WATI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"message": "Hello from n8n 👋"
}'Reply with media — POST /api/public/v1/send-media
Supported type: image, video, audio, document. The url must be a public HTTPS address (private/loopback URLs are blocked by SSRF guard).
curl -X POST https://wati.pro/api/public/v1/send-media \
-H "Authorization: Bearer $WATI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"type": "document",
"url": "https://example.com/invoice.pdf",
"fileName": "invoice.pdf",
"caption": "Your invoice 📄"
}'Test the signed webhook locally (ngrok)
Before pointing production traffic at n8n, dry-run the whole signature flow from your laptop. ngrok gives the Webhook node a public HTTPS URL that WATI Pro (and curl) can reach.
# 1. Start n8n locally (default port 5678)
n8n start
# 2. In another terminal, expose it over HTTPS
ngrok http 5678
# → Forwarding https://abc123.ngrok-free.app -> http://localhost:5678
# 3. Paste the HTTPS URL + your webhook path into
# WATI Pro → Settings → Incoming webhook URL:
# https://abc123.ngrok-free.app/webhook/wati-incomingSend a fake signed event and confirm Steps 2–3 accept it:
# Compute the signature for the sample body with your secret
BODY='{"from":"+15551234567","text":"ping","messageId":"test_1","deviceId":"local","receivedAt":"2026-06-28T11:32:01.245Z"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WATI_WEBHOOK_SECRET" -hex | awk '{print $2}')
curl -i -X POST https://abc123.ngrok-free.app/webhook/wati-incoming \
-H "Content-Type: application/json" \
-H "x-webhook-signature: sha256=$SIG" \
-d "$BODY"
# Expected: HTTP/1.1 200 OK (from your "Respond to Webhook" node)
# Bad secret / tampered body → Function node throws and n8n returns 500.If you see 200 OK the HMAC verify node is wired correctly. Flip one character in $BODY and resend — n8n should reject it with 500 Invalid WATI webhook signature.
Import the ready-made template
In n8n: Workflows → Import from File and pick the JSON below. It contains: Webhook → HMAC verify → IF branch (greeting vs media) → send / send-media → Respond 200.
wati-pro-workflow.jsonErrors you'll hit (and what they mean)
401 Invalid Bearer API key— token missing/revoked or wrong workspace.402 Plan limit reached— monthly message quota exhausted; upgrade or wait for renewal.409 Device is not connected— re-scan the WhatsApp device or pass a differentdeviceId.400 URL must be a public https:// address— send-media URL is private or non-HTTPS.- Invalid signature in n8n — secret mismatch or you're hashing the parsed JSON instead of the raw body.