Integration

WhatsApp Business API with n8n and Make: Official Automation

Tutorial: automate the WhatsApp Business API in n8n and Make with a signed webhook and REST API, with no ban risk. Includes an AI and CRM flow.

Person coding on a computer at a work desk
Illustrative photo · Pexels
In this article

To automate the WhatsApp Business API in n8n or Make you need two pieces: a webhook that delivers each message to your flow and an HTTP call that sends the reply through Meta's official API. There is no magic node. What changes compared to a session-based tool is that the number sits on the Cloud API, so sending follows Meta's rules (24-hour window, approved template outside it) and the risk of being blocked for "unauthorized automation" goes away. This guide builds four recipes, from a simple auto-reply to AI-powered support, with JSON ready to import into n8n.

What you will build

Any WhatsApp flow in n8n or Make boils down to three moves:

  1. Receive: the message arrives at a Webhook node, signed, with the raw body Meta delivered.
  2. Decide: the flow looks at what arrived, queries the CRM, calls an AI model or just matches a rule.
  3. Send: an HTTP Request node makes a POST to the REST API with free text or an approved template.

With a number connected to Frame Conexa, Meta's token never passes through n8n. The flow talks to https://frameconexa.com/api/v1 using a limited-scope fc_ token, and Conexa does the rest: it signs the webhook, deduplicates Meta's redeliveries, serializes the sending pace and returns the reason for each rejection. If you want to understand what sits underneath, the pillar article WhatsApp Cloud API: how to integrate with webhook and REST API explains each piece. Here the focus is the flow.

Why not use a session API in your flows

There are two families of "WhatsApp API". One is Meta's Cloud API, where the business registers the number on the official platform and sends messages over HTTP with public rules. The other is the session API: a server opens WhatsApp Web with your number, reads the screen and injects messages as if you were typing. The most popular tools of this kind in the Brazilian market fall here. The full comparison is in official vs unofficial WhatsApp API; for an automation flow, three differences weigh the most.

Meta prohibits automated sessions. WhatsApp's Messaging Guidelines ban "unofficial clients, bulk messaging, automatic messages or automation that harms WhatsApp or its users", with sanctions from a warning up to a permanent ban, and the Help Center points to the WhatsApp Business Platform as the legitimate alternative. An n8n flow running 24 hours a day on top of a session is exactly the pattern machine-learning detection looks for.

A session has no rules, and therefore no protection. On the official API, sending a free-form message outside the 24-hour window returns error 131047 and nothing goes out. On a session, it goes out. The flow "works" until the number is banned, with no intermediate signal.

A session drops on its own. Phone out of battery, WhatsApp Web logged out, app update: the flow stops and nobody is told. On the Cloud API the number lives on Meta's infrastructure, and when the authorization is revoked there is an event for it, which is recipe 4 in this guide.

If your number is on the phone app today, you do not have to give it up: the coexistence mode keeps the app working and ties the same number to the API.

Prerequisites: connected number, API token and webhook URL

Before the first node, three things need to exist:

Piece Where it comes from What it is for
Connected number Frame Conexa dashboard, "Connect WhatsApp" It is the instance that sends and receives. Its id is a UUID shown by GET /api/v1/instances
fc_ token Settings → API Authenticates each call from the flow: Authorization: Bearer fc_YOUR_TOKEN
Webhook URL and secret Settings → API, or PUT /api/v1/webhook-config It is where Conexa delivers events. The secret signs each delivery

The token is shown only once and the server stores just the hash. Create one token per flow: if one leaks, you revoke it without taking down the others. In n8n, store it in a Header Auth credential, never inside the node, so it does not travel along when you export the workflow. The full rules are in Authentication.

The webhook URL must be HTTPS and public. Private IPs, localhost, credentials in the URL and redirects are rejected when saving. On n8n Cloud the production URL of the Webhook node already works; on a self-hosted n8n you need a proxy with TLS in front.

Important: a free-form message can only go out inside the 24-hour window that the person opens by writing to you. Outside it, only a template approved by Meta. Every recipe below respects this, and it is worth reading the 24-hour window before designing any flow that sends hours after contact.

Recipe 1 (n8n): reply to incoming messages

The simplest flow: someone writes, n8n receives, validates the signature, builds a reply and sends it. Four nodes.

Webhook node, signature check, decision and send

Webhook node. Method POST, its own path, response "Immediately" and the Raw Body option on. The last two are mandatory. Responding right away is what the Conexa docs ask for: return 200 and process afterward. Without that, a slow AI node further down the line holds the response and the delivery goes into retry. The raw body is needed because the X-Frame-Signature-256 signature is an HMAC-SHA256 of the exact bytes that arrived; if n8n reformats the JSON beforehand, the hash never matches.

Code node. Recomputes the HMAC with the webhook secret, compares in constant time and discards what does not matter. This is where the recipe's most important decision lives: the messages event carries both incoming messages (payload.messages[]) and delivery receipts for yours (payload.statuses[]). Replying to a status is talking to yourself. And the smb_message_echoes type is what you yourself sent through the phone app; replying to it creates an infinite loop where the flow answers its own reply.

HTTP Request node. POST https://frameconexa.com/api/v1/instances/YOUR_INSTANCE/messages, authentication through the Header Auth credential, JSON body in the Cloud API format and the Idempotency-Key header with the incoming message id. That way, if Meta redelivers the webhook and n8n runs the flow again, the second send attempt comes back as a duplicate instead of sending two replies.

The Code node's code, the part that takes the most effort to get right:

javascript
const crypto = require('crypto');
const item = $input.first();
const raw = Buffer.from(item.binary.data.data, 'base64');
const received = item.json.headers['x-frame-signature-256'] || '';
const expected = 'sha256=' + crypto
  .createHmac('sha256', 'YOUR_WEBHOOK_SECRET')
  .update(raw)
  .digest('hex');

const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return [];

const event = JSON.parse(raw.toString('utf8'));
if (event.type !== 'messages') return [];
const msg = (event.payload.messages || [])[0];
if (!msg || msg.type !== 'text') return [];

return [{
  json: {
    idempotencyKey: msg.id,
    body: {
      to: msg.from,
      type: 'text',
      text: { body: `We received your message: "${msg.text.body}". We will reply shortly.` },
    },
  },
}];

Returning an empty array ends the execution on that branch: with no valid signature, or no incoming text message, nothing moves on to sending.

Note: n8n's Code node only sees the crypto module if the server is running with NODE_FUNCTION_ALLOW_BUILTIN=crypto. Without that permission, use the Crypto node (HMAC action, SHA256, hex output) on the raw body and compare with an IF node. The comparison is no longer constant-time, which is acceptable for a low-volume webhook but not for an endpoint exposed to lots of attempts.

Workflow JSON ready to import

Copy it, import it at Workflows → Import from file and swap in the instance id, the secret and the credential.

json
{
  "name": "Frame Conexa - reply to messages",
  "nodes": [
    {
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [0, 0],
      "parameters": {
        "httpMethod": "POST",
        "path": "frame-conexa",
        "responseMode": "onReceived",
        "options": { "rawBody": true }
      }
    },
    {
      "name": "Validate and decide",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [260, 0],
      "parameters": { "jsCode": "// paste the Code node code from above here" }
    },
    {
      "name": "Send reply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [520, 0],
      "credentials": { "httpHeaderAuth": { "name": "Frame Conexa" } },
      "parameters": {
        "method": "POST",
        "url": "https://frameconexa.com/api/v1/instances/YOUR_INSTANCE/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Idempotency-Key", "value": "={{ $json.idempotencyKey }}" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}"
      }
    }
  ],
  "connections": {
    "Webhook": { "main": [[{ "node": "Validate and decide", "type": "main", "index": 0 }]] },
    "Validate and decide": { "main": [[{ "node": "Send reply", "type": "main", "index": 0 }]] }
  }
}

The "Frame Conexa" credential is of the Header Auth type, with the name Authorization and the value Bearer fc_YOUR_TOKEN. After activating the workflow, paste the Webhook node's production URL into Settings → API in the dashboard, send a "hi" to the number from a phone and follow the execution.

Recipe 2 (n8n): AI support that respects the 24h window

Recipe 1 becomes an AI agent by swapping the fixed text for a model node (OpenAI, Anthropic or n8n's AI Agent node) between the Code node and the HTTP Request. The model receives the person's message, a system prompt with the business scope and, if you want, the last few messages of the conversation via GET /api/v1/instances/YOUR_INSTANCE/messages. It returns the text; the flow sends it.

What the AI cannot decide is the message type. That choice belongs to the flow.

When the AI can reply freely and when it needs a template

The rule is Meta's and has a single criterion: did the person send a message in the last 24 hours? If yes, free text. If not, only an approved template.

When the flow replies to an incoming-message webhook, the window is open by definition: the person just wrote. The problem shows up in flows that act later: the reminder that goes out four hours afterward, the next day's "still interested?", the follow-up to a stock lookup that took a while. For those, the flow needs to store the time of the last message received per contact (an n8n Data Table, a Postgres, a Redis) and compare before sending:

  • Less than 24 hours since the person's last message: type: "text", the model writes.
  • More than 24 hours: type: "template", with a template approved by Meta, and the model at most fills in variables within what the template allows.

Someone who arrived through a Click-to-WhatsApp ad has a longer window: if the business replies within 24 hours, Meta opens a 72-hour period in which any message is free. The contact carries a referral field in the first message, and the Conexa dashboard flags this window in the conversation. In your automation, confirm the free status from the status of each message sent: Meta marks pricing.type as free_entry_point when the message fell inside that window. With coexistence, a reply given from the phone app does not open this window; the first reply has to go out through the API. The details are in Click-to-WhatsApp ads.

Two cautions that are not about code. First, cost: so far a free-form reply inside the window is not charged by Meta, but from October 1, 2026 it will be charged per message, at the utility category rate for the recipient's country; an AI agent that sends five bubbles where one would do will cost five times more, and the article on WhatsApp Business API pricing tracks the table. Second, a human exit: the prompt needs a path for "I don't know, let me get someone", and the flow needs to alert the team in that case, because Meta measures blocks and reports, and a bot that insists is the fastest way to drag down the number's quality.

Tip: Conexa publishes an AI integration guide with ready-made prompts for the model to generate the webhook receiver and the sending client according to the API contract. It works for n8n as well as for your own code.

What Meta says about support bots and what an "AI Provider" is

Using AI to serve your own business is normal use of the platform. Meta's page on the subject cites as a permitted example a travel company operating a support bot. Scheduling, confirming an order, answering a product question, triage: all of that is customer service, with or without a language model behind it.

What Meta has started to restrict, according to the same page, are AI Providers: companies whose main product is a general-purpose AI assistant distributed through WhatsApp. That case has its own terms and billing. Meta also sells an AI agent of its own, the Meta Business Agent, with separate usage-based billing; it is a separate product and is not part of what this guide builds. Recipe 2 is on the side of ordinary customer service.

Recipe 3 (Make): CRM lead becomes a utility message

In Make the logic is the same, and the most useful recipe does not even need a webhook: the trigger is the CRM. A scenario with two modules:

  1. CRM trigger (HubSpot, Pipedrive, RD Station, whichever you use): "new deal" or "stage changed".
  2. HTTP → Make a request: POST to the Conexa API with a utility template, for example a confirmation that the proposal was sent.

Since the lead generally has not written to you in the last 24 hours, the message must be a template. The right category is utility, because the message is a consequence of an action by the person and not an offer. The body:

json
{
  "to": "5511999990000",
  "type": "template",
  "template": {
    "name": "proposal_sent",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Ana" },
          { "type": "text", "text": "PROP-2041" }
        ]
      }
    ]
  }
}

In the HTTP module, set the headers Authorization: Bearer fc_YOUR_TOKEN, Content-Type: application/json and Idempotency-Key with the deal id in the CRM, so a re-run of the scenario does not send twice. The phone goes in E.164 format without the plus sign: 5511999990000. The number of variables must be exactly that of the approved template, otherwise Meta rejects it with code 132000.

Two points on policy. A utility template still requires that the person has given you their number and agreed to receive messages from your company; consent comes from Meta's Business Messaging Policy, not from the provider. And if the flow is a commercial invitation, rather than a consequence of something the person did, the category is marketing, which requires a marketing number in Conexa and falls under the rules of official bulk messaging.

To receive webhooks in Make, the Custom Webhook module works, but check whether your version can expose the raw body for the HMAC calculation. If it cannot, use a long random path in the URL as a minimum barrier and treat the content as untrusted: before any action with side effects, confirm the message through the API with GET /instances/:id/messages.

Recipe 4: alert the team when a number goes down

Every Cloud API number can lose its authorization: the owner revokes access in Business Manager, Meta removes the partner, or a subscription expires. When that happens, sending starts returning 409 and nothing else goes out. Without an alert, the team finds out from the customer who complained.

Conexa emits an instance_status event on the same webhook, with the same envelope as the others:

json
{
  "type": "instance_status",
  "clientId": "8",
  "instanceId": "6ed76b84-4d65-4f3d-88c1-8d4174bdce73",
  "customerId": null,
  "customerRef": null,
  "payload": {
    "status": "disconnected",
    "phoneNumberId": "1234567890",
    "displayPhone": "+55 11 99999-0000",
    "kind": "marketing",
    "disconnectReason": {
      "code": "token_revoked",
      "message": "A autorização do Frame Conexa na Meta expirou ou foi revogada. Reconecte este número para emitir uma nova autorização."
    }
  }
}

The flow is short: in the Code node from recipe 1, add a branch for event.type === 'instance_status' and send payload.displayPhone, payload.status and payload.disconnectReason.message to a Slack, email or Telegram node. The codes that arrive in disconnectReason.code are token_revoked, account_offboarded, partner_removed, removed_from_frame_conexa and subscription_expired, and the message already comes written to be shown to a person (in Portuguese in the example above).

When someone reconnects the number in the dashboard, an instance_status arrives with status: "connected". It is the event that lets the flow start sending again on its own, without anyone having to remember to reactivate it. Store the state by instanceId, which is the same UUID as in GET /api/v1/instances; clientId exists for compatibility and does not appear in any API response.

Warning: a flow that sends with the number down accumulates 409 errors and, if it has automatic retry, can fire dozens of useless calls per minute. When you receive disconnected, pause the sending scenarios; when you receive connected, resume.

Common errors in flows

The errors below show up in almost every new flow. The "whose" column matters: the natural suspicion is that the limit belongs to the provider, and most of the time it is Meta's.

Symptom Code Whose What to do
Flow replies to its own message nonstop none the flow's Ignore smb_message_echoes and payload.statuses; reply only to payload.messages
Same reply sent twice none Meta's, expected Redeliveries are normal. Use messages[].id as the Idempotency-Key
401 on every call 401 the token's Token missing, revoked or pasted with a space. Generate another in Settings → API
"instance disconnected" 409 (Graph 190) the authorization at Meta Reconnect the number in the dashboard. Recipe 4 warns you beforehand
Free-form message rejected 131047 Meta's The 24-hour window closed. Send an approved template
Recipient with no delivery 131026 Meta's The number has no active WhatsApp. Remove it from the audience; resending gives the same result
Marketing not delivered 131049 Meta's, per person The person already received too much marketing across all companies. Wait at least 24 hours
Variables do not match 132000 the template's The number of parameters is not that of the approved template
Too many calls 429 Conexa's 120 requests per minute per token. Respect the Retry-After
"uncertain result" 502 with ambiguous: true the network's or Meta's The message may have gone out. Do not resend blindly; check GET .../messages

The last one deserves a rule in the flow: n8n's HTTP Request automatic retry must be off for this route, or conditioned on ambiguous being absent. With an Idempotency-Key on the send, repeating the same key and the same body is safe and returns 409 SEND_IN_PROGRESS while the first is still running; without the key, each repeat is a new message. The full dictionary, with what to do in each case, is in WhatsApp API errors and in Errors and limits.

Frequently asked questions

Can I use n8n's native WhatsApp Business Cloud node with Frame Conexa?

Not directly. n8n's native node talks to Meta's Graph API using your account's access token and phone number id, which are exactly what Conexa keeps encrypted so the number does not depend on a loose token in a workflow. With Conexa, sending goes through the HTTP Request node against https://frameconexa.com/api/v1, and receiving goes through the generic Webhook node with Raw Body on, as in recipe 1. The send body format is the same as the Cloud API's, so anyone who has already built a payload for the native node only swaps the URL and the header.

Can my AI bot reply to any message on WhatsApp?

It can reply to whatever counts as customer service for your business, inside the 24-hour window opened by the person. That is the use Meta describes as permitted, and its example is a support bot for a travel company. What Meta restricts, according to the page on AI Providers, is offering a general-purpose AI assistant as a product through WhatsApp. And outside the window neither a bot nor a person sends free text: it is an approved template or nothing.

What happens if the flow sends a message outside the 24h window?

Meta rejects it with code 131047 and the message does not go out. It is not charged, because Meta only charges for delivered messages, but it does not arrive either. Conexa returns the error in the response body with Meta's code, and the dashboard shows the reason as "window closed". The fix is in the flow design: store the time of the last message received per contact and switch to type: "template" when more than 24 hours have passed.

How do I know in n8n whether the message was delivered?

Through the delivery receipt, which arrives on the same webhook. The response to the POST carries the message id in result.messages[0].id, a value that starts with wamid.. After that, Conexa delivers messages events with payload.statuses[], each with the same id and a status that moves through sent, delivered, read or failed; on failed, the reason comes in errors[]. Store the id when sending and match it on the webhook. If you prefer to query rather than listen, GET /api/v1/instances/:id/messages lists the messages with their current state.

Do I need a VPS to run the automation?

No, as long as the webhook has a public HTTPS URL. n8n Cloud and Make already provide that. An n8n installed on your own server needs a proxy with a TLS certificate in front, because Conexa rejects URLs without HTTPS, with a private IP or pointing to localhost. What does not work is running n8n on your laptop and expecting the webhook to arrive: the delivery needs to find the address on the internet.

Next step

Connect a number and generate your first token in the per-number plans; the webhook and the API come in the same plan, with no per-message charge from us. With the token in hand, the quickstart shows the first call in a minute, and recipe 1 above is the workflow to import next.

Keep reading

Connect the number you already use to the official WhatsApp API.

No switching numbers, no losing the app and no per-message fee from us. You pay per number; you pay Meta directly.