· 8 min read · Nadeera Silva
WhatsApp AI Agent for Business: Cloud API, Webhooks and CRM
Build a WhatsApp AI agent on the Cloud API that updates your CRM: webhook setup, the 24-hour rule, templates, human handoff and October 2026 pricing.

A WhatsApp AI agent for a business usually means three small systems: a webhook that receives messages from the WhatsApp Cloud API, a worker that turns a conversation into CRM fields, and replies a person can trust. This post is how I build that, with code for the parts that break most often: webhook setup, duplicates, the 24-hour rule, templates and human handoff. Everything here follows Meta's documentation and terms as of October 2026, linked at the end so you can check for changes.
WhatsApp rules to design around first
Four rules from Meta shape the design, and each is easier to build in from the start:
- Opt-in. The WhatsApp Business Messaging Policy says you may only contact people who gave you their number and opted in to messages from you.
- The 24-hour rule. When a user messages you, a 24-hour customer service window opens and resets with each new message. Inside it you can send free-form replies. Outside it you can only send approved template messages.
- Escalation paths. Automation is allowed, but the policy says you "must also have available prompt, clear, and direct escalation paths", such as a transfer to a human agent, a phone number or email.
- AI providers. Section 4.7 of the Meta Terms for WhatsApp Business Platform (last modified 23 September 2026) prohibits providers of AI technologies, including general-purpose AI assistants, from using the platform when that AI is the primary functionality being offered, "as determined by Meta in its sole discretion". A business using AI to qualify its own leads is a different case from selling a chatbot product, but read the clause yourself or with a lawyer if you're near that line.
WhatsApp Cloud API pricing in 2026
Meta charges per delivered message, by category and country, and doesn't charge for messages users send you. One change matters for any AI agent:
So an agent that replies to every message now has a running cost per reply. Check the rates for your countries on Meta's pricing page, and add a payment method before you launch.
| Message | When you can send it | Charged? |
|---|---|---|
| User to business | Any time | No |
| Free-form reply (service) | Inside the 24-hour window | Yes, after 1,000 free per number per month |
| Utility template | Any time, with opt-in | Yes, including inside the window |
| Marketing or authentication template | Any time, with opt-in | Yes |
Architecture of a WhatsApp AI agent

The webhook only verifies, stores and acknowledges. Anything slow, such as a model call or a CRM API, happens in a worker reading from a queue. That split matters because Meta retries a webhook that doesn't get a 200 for up to 7 days, and retries can produce duplicates.
WhatsApp webhook setup
- In the App Dashboard, open WhatsApp > Configuration (or Use cases > Customize > Configuration if you created the app with the "Connect with customers through WhatsApp" use case).
- Enter your callback URL and a verify token you choose.
- Subscribe to the
messagesfield. Without it, you won't receive incoming messages.
Meta checks the endpoint with a GET request carrying hub.mode, hub.verify_token and hub.challenge. You compare the token and echo the challenge. Every POST is signed with your app secret in X-Hub-Signature-256. Meta's docs say you don't have to validate it, "but you should".
import crypto from "node:crypto";
import express from "express";
const app = express();
app.get("/whatsapp/webhook", (req, res) => {
if (
req.query["hub.mode"] === "subscribe" &&
req.query["hub.verify_token"] === process.env.WA_VERIFY_TOKEN
) {
return res.status(200).send(req.query["hub.challenge"]);
}
res.sendStatus(403);
});
app.post(
"/whatsapp/webhook",
express.raw({ type: "application/json" }), // the signature covers the raw bytes
async (req, res) => {
const header = req.get("x-hub-signature-256") ?? "";
const expected =
"sha256=" +
crypto.createHmac("sha256", process.env.WA_APP_SECRET!)
.update(req.body)
.digest("hex");
const ok =
header.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
await queue.add("wa-event", JSON.parse(req.body.toString("utf8")));
res.sendStatus(200);
}
);WhatsApp webhook not receiving messages?
| Symptom | Usual cause |
|---|---|
| Verification fails in the dashboard | The verify token doesn't match, or the endpoint returns JSON instead of the raw challenge |
| Verified, but no POSTs arrive | The messages field isn't subscribed |
| POSTs arrive, signature check always fails | The body was parsed as JSON before hashing, or you're using the wrong app's secret |
| The same message is processed twice | Your endpoint was slow or failed, so Meta retried |
The worker: from message to CRM fields
An incoming text message sits under entry[].changes[].value.messages[], with the sender in from, an ID starting with wamid. and the text in text.body. The worker:
- Skips any message ID it has already processed.
- Loads the contact from the CRM by WhatsApp ID, so the model knows whether this is a new lead.
- Asks the model for structured fields and validates them.
- Upserts the CRM record and logs what changed.
import { z } from "zod";
const Lead = z.object({
name: z.string().min(1).optional(),
company: z.string().optional(),
budget: z.enum(["<1k", "1k-5k", "5k+", "unknown"]),
wantsHuman: z.boolean(),
confidence: z.number().min(0).max(1),
});
export async function handleEvent(event: WaEvent) {
for (const entry of event.entry) {
for (const change of entry.changes) {
for (const msg of change.value.messages ?? []) {
if (msg.type !== "text") continue;
if (!(await markSeen(msg.id))) continue; // duplicate delivery
const contact = await crm.findByWhatsAppId(msg.from);
const raw = await extractLeadWithModel(msg.text.body, contact);
const lead = Lead.safeParse(raw);
if (!lead.success || lead.data.wantsHuman || lead.data.confidence < 0.7) {
await handoffToHuman(msg.from, msg.text.body, contact);
continue;
}
await crm.upsertByWhatsAppId(msg.from, lead.data);
await sendText(msg.from, "Thanks! I've noted that. A colleague will follow up today.");
}
}
}
}extractLeadWithModel is whichever model you use, asked for JSON matching the schema. The safeParse is the important line: model output is untrusted input, and nothing reaches the CRM without passing validation.
Sending replies and template messages
Inside the window, a text message is enough (bodies can be up to 4,096 characters):
async function send(payload: object) {
const res = await fetch(
`https://graph.facebook.com/v26.0/${process.env.WA_PHONE_NUMBER_ID}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WA_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ messaging_product: "whatsapp", recipient_type: "individual", ...payload }),
}
);
if (!res.ok) throw new Error(`WhatsApp send failed: ${res.status}`);
}
const sendText = (to: string, body: string) =>
send({ to, type: "text", text: { body } });Outside the window, send an approved template instead. With named parameters:
const sendFollowUp = (to: string, firstName: string) =>
send({
to,
type: "template",
template: {
name: "lead_follow_up", // must be APPROVED in WhatsApp Manager
language: { code: "en_US" },
components: [{
type: "body",
parameters: [{ type: "text", parameter_name: "first_name", text: firstName }],
}],
},
});Pin the Graph API version (Meta's examples use v26.0) and plan upgrades. Meta also notes that delivery order isn't guaranteed to match request order, so if two messages must arrive in sequence, wait for the first one's status webhook.
Human handoff that works
- Hand off when the user asks for a person, when validation fails or when confidence is low. The worker above does all three.
- Give the person taking over the conversation, the CRM record and what the agent already tried.
- Stop automated replies while a human owns the conversation. A flag on the contact is enough.
Data and privacy
The Business Messaging Policy makes you responsible for the notices and consents needed to collect and use chat data. Keep raw message bodies only as long as needed, mask phone numbers in logs, keep the app secret and access token on the server, and know exactly which fields go to your model provider.
Testing checklist
- GET verification echoes the challenge only with the right token.
- A POST with a wrong signature returns 401 and changes nothing.
- The same webhook sent twice updates the CRM once.
- "Can I talk to a person?" reaches a human and pauses automation.
- A follow-up after 24 hours uses a template.
- Monthly service message counts are visible somewhere, so the October 2026 charges don't surprise anyone.
Adding retrieval so the agent answers from your own documents? My post on RAG with citations in pgvector covers grounding. The portfolio AI chat write-up shows the guardrails I use for a public AI chat.
Frequently asked questions
Is the WhatsApp Cloud API free?
No. Meta charges per delivered message by category and country. Since 1 October 2026, service replies are charged too, after 1,000 free service messages per business phone number per month.
What is the WhatsApp 24-hour rule?
When a user messages you, a 24-hour customer service window opens and resets with each new message. Inside it you can send free-form replies. Outside it you can only send approved templates.
How do I set up a WhatsApp webhook?
Add your callback URL and verify token in the App Dashboard's WhatsApp Configuration, echo hub.challenge on the GET request, and subscribe to the messages field.
Why is my WhatsApp webhook not receiving messages?
Most often the messages field isn't subscribed. Also check that verification succeeded and that the endpoint returns 200 quickly.
How do I send a WhatsApp template message?
POST to the Messages API with type set to template, the approved template name, a language code and any parameters.
Does Meta allow AI chatbots on WhatsApp?
Meta's terms prohibit AI providers from using the platform when the AI, such as a general-purpose assistant, is the primary functionality offered. Businesses using AI for their own customer conversations should still read section 4.7.
Why am I getting duplicate webhooks?
Meta retries webhooks that don't get a 200 for up to 7 days, and retries can produce duplicates. Store processed message IDs.
Sources
- WhatsApp Business Messaging Policy (Meta)
- Meta Terms for WhatsApp Business Platform (section 4.7) (Meta, 2026-09-23)
- WhatsApp webhooks overview (Meta)
- Getting started with Webhooks (verification and payload signing) (Meta)
- WhatsApp pricing (customer service window) (Meta)
- Text messages (Meta)
- Template fundamentals (Meta)
Keep reading
- RAG with Citations on PostgreSQL and pgvector: How I Built ItHow I built RAG with citations on PostgreSQL and pgvector for Pactroom: schema, project-scoped retrieval, citation prompts and the HNSW filtering trap.
- Building a Next.js Portfolio Website with Firestore and an AI ChatbotHow I built my Next.js portfolio on Firebase: Firestore content with typed fallbacks, SEO metadata, a guarded AI chatbot and a contact form that checks errors.
- Flutter Stripe Integration: PaymentSheet, Webhooks and ConnectIntegrate Stripe in Flutter with flutter_stripe: PaymentSheet, Apple Pay and Google Pay, webhooks that fulfil orders, and Stripe Connect for marketplaces.
Comments
Share a question or note about this article.
Loading comments…