← All posts
Tutorial

Build an n8n WhatsApp AI Agent From a Signed Inbound Webhook

The fastest way to build a WhatsApp AI agent in 2026 is usually not to start with an agent. Start with the inbound edge.

The public pain is easy to find. In one recent n8n community thread on Reddit, a builder wanted WhatsApp automation with DeepSeek, media handling, and production stability, but did not want to get stuck in the Meta Business verification and Cloud API setup path. In another thread, people were debugging WhatsApp triggers that worked once, worked only in test mode, or failed because webhook delivery and workflow response timing were not aligned.

That is exactly the order of operations small teams should fix: receive the message reliably, acknowledge it quickly, store it, and only then let the AI agent decide whether to reply.

UnifyPort gives you the inbound piece as a signed message.received event. n8n gives you the workflow canvas. Put them together with a small edge verifier and you get a production-friendly path:

WhatsApp customer message
  -> UnifyPort signed message.received webhook
  -> Edge verifier for X-Device-Signature
  -> n8n production Webhook URL
  -> AI triage, CRM lookup, Slack alert, or reply via POST /v1/messages

Why the n8n webhook URL matters

n8n’s official Webhook node documentation is clear that a Webhook node has two URLs: a test URL and a production URL. The test URL is for manual runs while the editor is listening. The production URL is the one you use after the workflow is active.

For a customer-message pipeline, always design around the production URL. A WhatsApp customer is not going to resend a message because the editor was not listening. Your inbound path should be active, stable, and able to return a 2xx response quickly.

The n8n Respond to Webhook node is useful when the workflow itself owns the HTTP response. For inbound messaging, keep that response simple: accept the event, enqueue or store it, and return 200. Long AI reasoning, CRM writes, and outbound replies should happen after the delivery has been acknowledged.

Register the UnifyPort webhook

Create a webhook endpoint in UnifyPort and subscribe to message.received. Set a signing_secret so each delivery includes X-Device-Timestamp and X-Device-Signature.

curl -X POST https://api.unifyport.ai/v1/webhook-endpoints \
  -H "X-Api-Key: <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://edge.example.com/unifyport/n8n",
  "status": "active",
  "subscribed_events": ["message.received"],
  "signing_secret": "<WEBHOOK_SIGNING_SECRET>"
}'

The URL above is not the n8n URL directly. It is a tiny edge verifier that sits in front of n8n. That keeps signature verification strict because UnifyPort signs the raw request body with HMAC-SHA256. The signed string is:

<X-Device-Timestamp>.<raw request body>

If your n8n deployment can expose the exact raw request bytes before JSON parsing, you can verify inside n8n. Many teams keep the boundary cleaner by verifying in a small service and forwarding only trusted events to the n8n production webhook with an internal token.

Add the edge verifier

Here is the complete verifier in Node.js. It verifies the UnifyPort signature, parses the event, checks the event type, and forwards the trusted payload to n8n.

import crypto from "crypto";
import express from "express";

const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;
const n8nWebhookUrl = process.env.N8N_PRODUCTION_WEBHOOK_URL;
const internalToken = process.env.N8N_INTERNAL_TOKEN;

app.post("/unifyport/n8n", express.raw({ type: "application/json" }), async (req, res) => {
  const timestamp = req.get("X-Device-Timestamp") || "";
  const signature = req.get("X-Device-Signature") || "";

  const hmac = crypto.createHmac("sha256", signingSecret);
  hmac.update(timestamp);
  hmac.update(".");
  hmac.update(req.body);
  const expected = hmac.digest("hex");

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!valid) {
    res.status(401).end();
    return;
  }

  const event = JSON.parse(req.body.toString("utf8"));
  if (event.type !== "message.received") {
    res.status(202).end();
    return;
  }

  await fetch(n8nWebhookUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Internal-Token": internalToken
    },
    body: JSON.stringify(event)
  });

  res.status(200).end();
});

app.listen(3000);

This service does not store credentials from the WhatsApp account, and it does not decide how the agent should answer. Its only job is to prove that the event came from your UnifyPort endpoint and hand a trusted event to n8n.

Build the n8n workflow

In n8n, create an active workflow with a Webhook trigger using the production URL. The incoming JSON is already the standard UnifyPort event:

{
  "id": "evt_2f9c1a4b7e",
  "type": "message.received",
  "provider": "whatsapp",
  "account_id": "acc_8c21d0",
  "occurred_at": "2026-07-08T02:30:00Z",
  "data": {
    "conversation": { "id": "8613912345678", "type": "user", "title": "Jordan Lee" },
    "sender": { "id": "8613912345678", "name": "Jordan Lee", "type": "user" },
    "message": {
      "id": "wamid.HBgM",
      "type": "text",
      "text": "Can I change the delivery address?",
      "direction": "inbound",
      "sent_at": "2026-07-08T02:29:59Z"
    },
    "event": { "kind": "message_received" }
  }
}

A practical first workflow has five nodes:

  1. Webhook: receives the forwarded event.
  2. IF: confirms type is message.received and provider is whatsapp.
  3. Data store, Postgres, Airtable, or CRM: stores id, account_id, provider, data.conversation.id, data.sender.id, and data.message.text.
  4. AI Agent or HTTP Request to your model gateway: classifies the message as sales, support, billing, or human handoff.
  5. HTTP Request: optionally replies through UnifyPort.

Store first. UnifyPort’s webhook documentation treats events as the record of inbound traffic. There is no message-history read API for missed payloads, so your workflow should persist the event before a slow AI step can fail.

Reply only after the workflow decides

If the agent should answer, use POST /v1/messages. Keep the recipient from the inbound event and keep the reply explicit.

curl -X POST https://api.unifyport.ai/v1/messages \
  -H "X-Api-Key: <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "account_id": "acc_8c21d0",
  "to": { "id": "8613912345678", "type": "user" },
  "message": {
    "type": "text",
    "text": "Yes. Send us the new delivery address and we will update the order note."
  }
}'

In n8n, that is an HTTP Request node. Map account_id from {{$json.account_id}}, map the recipient id from {{$json.data.sender.id}}, and set message.text from the AI node output.

Why this is better than making the agent the first hop

An AI agent should not be the first system that touches a customer message. The first system should be boring: verify, acknowledge, store, and route. That keeps the operational contract stable even when the AI model, prompt, or escalation policy changes.

This structure also lets the same n8n workflow expand beyond WhatsApp. The envelope uses provider, account_id, occurred_at, and data. When you later connect Telegram, LINE, Zalo, TikTok, or X, the workflow can branch on provider without learning six webhook formats.

The official WhatsApp Business Developer Hub remains the right place to learn Meta’s Cloud API, webhooks, pricing, and policy surface. Use it when you are building on the official platform. But if your team needs an inbound WhatsApp support agent without carrying every official setup step into n8n, UnifyPort’s unofficial interface gives the workflow a narrower job: receive signed customer messages and hand them to the tools your team already uses.

Start with the edge. Once the edge is reliable, the agent becomes just another node.