Skip to content

How to build a WhatsApp AI bot with Claude, GPT or Gemini

Connect the official WhatsApp Cloud API to Claude, GPT or Gemini with a small Node.js server. Tested code, Meta's rules, safety limits and costs.

Updated November 1, 2026Markdown version

To build a WhatsApp AI bot, connect Meta's official WhatsApp Cloud API to a small server of your own. WhatsApp sends each customer message to the server, which asks the model for a reply and sends it back through the Cloud API. With a 5abeer key, the OpenAI SDK reaches Claude, GPT and Gemini models below official prices.

What do you need to build a WhatsApp AI bot?#

  • A Meta developer app with the WhatsApp product, which comes with a test business number.
  • A 5abeer API key for the bot's provider. The code uses the default model, claude-sonnet-5.5, so it needs a Claude key. See Create a key and How to get an API key for Claude, GPT and Gemini.
  • Node.js 22 or later, with the express and openai packages.
  • A server that stays on, with a public HTTPS address. Meta asks for "a valid TLS or SSL certificate", and "Self-signed certificates are not supported."

Which way should you connect to WhatsApp?#

The three common ways, checked on 2026-11-01:

WayWhat it isFor a business bot
WhatsApp Cloud APIMeta's own API for business numbers, used hereYes: official, and the code is yours
A Business Solution ProviderA company that connects you to the platform, with its own toolsYes, for a shared inbox or setup help; check its fees
An unofficial WhatsApp Web libraryCode that logs in as an ordinary account and automates itNo: it breaks WhatsApp's terms

WhatsApp's Terms of Service forbid "bulk messaging, auto-messaging, auto-dialing" and "any non-personal use of our Services unless otherwise authorized by us", so a bot on an ordinary account puts the number at risk.

Does Meta allow AI bots on WhatsApp?#

Yes, for a business answering its own customers. Meta's terms for the WhatsApp Business Platform, updated on September 23, 2026, bar AI providers when AI is "the primary (rather than incidental or ancillary) functionality" of what they offer. Meta's AI providers page lets AI providers offer general-purpose AI assistants only "where Meta is legally required to permit this use case."

A store bot answering questions about its own orders and products uses AI as part of its customer service, which is the case here. The terms also limit using your WhatsApp data to train AI systems.

5abeer is only the model API your server calls. It doesn't connect to WhatsApp or to your Meta account. This is general information, not legal advice, so read Meta's terms before you launch.

How do you connect the server to Claude, GPT or Gemini?#

5abeer speaks the OpenAI Chat Completions format, so the server uses the official OpenAI SDK with a different base URL and key. The same setup runs a website chatbot and a Telegram AI bot.

OpenAI-compatiblehttps://api.5abeer.ai/v1Authorization: Bearer <KEY>
Anthropic-compatiblehttps://api.5abeer.aix-api-key: <KEY>
Geminihttps://api.5abeer.ai/v1betax-goog-api-key: <KEY>

Send one test request first to check the key and the model. The format is in Chat Completions.

curl --request POST "https://api.5abeer.ai/v1/chat/completions" \
  --header "Authorization: Bearer $KHABEER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"model":"claude-sonnet-5.5","messages":[{"role":"user","content":"Write a product idea in one sentence."}]}'
Response
{
  "id": "chatcmpl-9f2",
  "object": "chat.completion",
  "model": "claude-sonnet-5.5",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello! How can I help you today?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 21,
    "completion_tokens": 12,
    "total_tokens": 33
  }
}

The key is read from KHABEER_API_KEY, so it never appears in your code.

What does the server code look like?#

The bot is one file. Meta checks your address with GET /webhook and delivers messages with POST /webhook.

server.mjs
import crypto from "node:crypto";
import express from "express";
import OpenAI from "openai";

const env = process.env;
const missing = ["WHATSAPP_TOKEN", "WHATSAPP_APP_SECRET", "WHATSAPP_VERIFY_TOKEN", "WHATSAPP_PHONE_NUMBER_ID", "KHABEER_API_KEY"].filter((name) => !env[name]);
if (missing.length) {
  console.error("Set these environment variables first:", missing.join(", "));
  process.exit(1);
}

const client = new OpenAI({ apiKey: env.KHABEER_API_KEY, baseURL: "https://api.5abeer.ai/v1" });
const MODEL = env.CHAT_MODEL || "claude-sonnet-5.5";
const GRAPH = `https://graph.facebook.com/v26.0/${env.WHATSAPP_PHONE_NUMBER_ID}/messages`;

// One business answering its own customers. Replace the example store's facts with yours.
const SYSTEM = `You are the WhatsApp assistant of Example Store.
Answer only questions about the store, in the customer's language, briefly and in plain text.
Opening hours: 9:00 to 21:00, Saturday to Thursday. Shipping takes 2 to 4 days.
If you don't know the answer, say so and suggest the contact page on the store's website.`;

const MAX_TURNS = 10; // the last 5 questions and answers per customer
const MAX_CHARS = 4000; // per customer message
const LIMIT = 20; // messages per customer every 10 minutes
const MEDIA = new Set(["audio", "document", "image", "sticker", "video"]);
const history = new Map(); // in memory: lost when the server restarts
const hits = new Map();
const seen = new Set();
const queues = new Map();
setInterval(() => hits.clear(), 10 * 60 * 1000).unref();

// Meta signs every POST with your app secret: X-Hub-Signature-256 is "sha256=" + the HMAC of the raw body.
function signed(req) {
  if (!Buffer.isBuffer(req.body)) return false;
  const expected = Buffer.from("sha256=" + crypto.createHmac("sha256", env.WHATSAPP_APP_SECRET).update(req.body).digest("hex"));
  const received = Buffer.from(req.get("x-hub-signature-256") ?? "");
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

async function send(customer, text) {
  // A WhatsApp text message holds at most 4,096 characters, so long answers go in parts.
  for (const part of text.match(/[\s\S]{1,4000}/gu)) {
    const res = await fetch(GRAPH, {
      method: "POST",
      headers: { Authorization: `Bearer ${env.WHATSAPP_TOKEN}`, "Content-Type": "application/json" },
      body: JSON.stringify({ messaging_product: "whatsapp", recipient_type: "individual", ...customer, type: "text", text: { body: part } }),
      signal: AbortSignal.timeout(15000),
    });
    if (!res.ok) throw new Error(`WhatsApp API ${res.status}: ${await res.text()}`);
  }
}

async function answer(id, customer, message, overLimit) {
  if (overLimit) return send(customer, "You've sent many messages. Please wait a few minutes, then try again.");
  if (MEDIA.has(message.type)) return send(customer, "Sorry, I can only read text messages.");
  if (message.type !== "text") return; // reactions, locations, system notices...
  const past = history.get(id) ?? [];
  const question = String(message.text?.body ?? "").slice(0, MAX_CHARS);
  let reply;
  try {
    const completion = await client.chat.completions.create({
      model: MODEL,
      messages: [{ role: "system", content: SYSTEM }, ...past, { role: "user", content: question }],
      max_tokens: 800,
    });
    reply = (completion.choices[0]?.message?.content ?? "").trim();
    if (!reply) throw new Error("empty answer");
  } catch (error) {
    console.error("model error:", error.message);
    return send(customer, "Sorry, I can't answer right now. Please try again in a minute.");
  }
  history.delete(id); // re-insert, so the oldest conversation comes first
  history.set(id, [...past, { role: "user", content: question }, { role: "assistant", content: reply }].slice(-MAX_TURNS));
  if (history.size > 10000) history.delete(history.keys().next().value);
  await send(customer, reply);
}

function receive(message) {
  if (seen.has(message.id)) return; // Meta retries failed deliveries, so a message can arrive twice
  seen.add(message.id);
  if (seen.size > 10000) seen.delete(seen.values().next().value);
  // Reply to the phone number, or to the business-scoped user ID when WhatsApp hides the number.
  const id = message.from_user_id ?? message.from;
  if (!id) return;
  const customer = message.from ? { to: message.from } : { recipient: message.from_user_id };
  const count = (hits.get(id) ?? 0) + 1;
  hits.set(id, count);
  if (count > LIMIT + 1) return; // stay silent: every reply is a message Meta may charge for
  // One queue per customer: answers go out in order, and each one sees the previous answer.
  const job = (queues.get(id) ?? Promise.resolve())
    .then(() => answer(id, customer, message, count > LIMIT))
    .catch((error) => console.error("reply error:", error.message))
    .finally(() => queues.get(id) === job && queues.delete(id));
  queues.set(id, job);
}

const app = express();

// Meta calls this once, when you save the callback URL in the App Dashboard.
app.get("/webhook", (req, res) => {
  const ok = req.query["hub.mode"] === "subscribe" && req.query["hub.verify_token"] === env.WHATSAPP_VERIFY_TOKEN;
  if (!ok) return res.sendStatus(403);
  res.type("text/plain").send(String(req.query["hub.challenge"]));
});

// Meta can batch up to 1,000 updates in one POST.
app.post("/webhook", express.raw({ type: "application/json", limit: "5mb" }), (req, res) => {
  if (!signed(req)) return res.sendStatus(401);
  let body;
  try {
    body = JSON.parse(req.body.toString("utf8"));
  } catch {
    return res.sendStatus(400);
  }
  res.sendStatus(200); // answer Meta first; the replies go out afterwards
  for (const entry of body?.entry ?? []) {
    for (const change of entry.changes ?? []) {
      if (change.value?.metadata?.phone_number_id !== env.WHATSAPP_PHONE_NUMBER_ID) continue;
      for (const message of change.value.messages ?? []) receive(message); // status updates have no "messages"
    }
  }
});

app.listen(env.PORT || 3000, () => console.log("WhatsApp bot running on port", env.PORT || 3000));

We tested this file on 2026-11-01 with Node.js 22, Express 5.3 and the openai SDK 7.47, against local stand-ins for the API and the WhatsApp Cloud API, with signed test webhooks and fake keys. It calls v26.0 of Meta's Graph API, the newest in Meta's changelog that day.

How do you set up the Meta app?#

  1. In the Meta App Dashboard, create an app with the use case "Connect with customers through WhatsApp".
  2. On the WhatsApp API Setup page, copy the phone number ID. You can add your business number later.
  3. Copy the App Secret from your app's settings. The server uses it to check that each delivery comes from Meta.
  4. The temporary token on API Setup expires quickly. For a lasting one, add a system user in Business Settings and generate a token with the business_management, whatsapp_business_management and whatsapp_business_messaging permissions.
  5. Pick a long random string as your verify token.
  6. With the server running, open App Dashboard > Use cases > Customize > Configuration, set the callback URL to your HTTPS address followed by /webhook, enter the verify token and subscribe to the messages field. Meta then calls the address and the server returns the challenge.

How do you run it?#

Run these commands in the folder with server.mjs, with your own values:

macOS / Linux
npm install express openai
export KHABEER_API_KEY="YOUR_5ABEER_API_KEY"
export WHATSAPP_TOKEN="YOUR_SYSTEM_USER_TOKEN"
export WHATSAPP_APP_SECRET="YOUR_APP_SECRET"
export WHATSAPP_VERIFY_TOKEN="YOUR_VERIFY_TOKEN"
export WHATSAPP_PHONE_NUMBER_ID="YOUR_PHONE_NUMBER_ID"
node server.mjs
Windows
npm install express openai
$env:KHABEER_API_KEY="YOUR_5ABEER_API_KEY"
$env:WHATSAPP_TOKEN="YOUR_SYSTEM_USER_TOKEN"
$env:WHATSAPP_APP_SECRET="YOUR_APP_SECRET"
$env:WHATSAPP_VERIFY_TOKEN="YOUR_VERIFY_TOKEN"
$env:WHATSAPP_PHONE_NUMBER_ID="YOUR_PHONE_NUMBER_ID"
node server.mjs

Meta can't reach localhost, so to test on your computer, open an HTTPS tunnel to port 3000, for example with Cloudflare Tunnel, and use its address as the callback URL. To change the model or the port, set CHAT_MODEL or PORT.

How does the code work?#

  • Signatures: Meta signs every POST with your App Secret in the X-Hub-Signature-256 header. The server checks it against the raw body and rejects anything unsigned with a 401.
  • Fast answer: the server returns 200 first, then replies. Meta resends updates that don't get a 200, so the server skips message ids it has already seen.
  • Memory and order: each new question goes with the customer's last 5 questions and answers, and one queue per customer keeps answers in order. The memory resets on restart.
  • Media: images, audio, video, documents and stickers get a note that the bot reads only text.
  • Length: each customer message is cut to 4,000 characters. A WhatsApp text holds at most 4,096 characters, so longer answers go out in parts.
  • Rate limit: each customer gets 20 messages every 10 minutes. The 21st gets one "please wait" note, and later ones get no reply.
  • Errors: the SDK retries the model twice, then the customer gets an apology. max_tokens: 800 caps Claude and Gemini answers, and GPT models ignore it, as in Parameters.
  • Usernames: when WhatsApp hides a number, the server replies to the business-scoped user ID with recipient.

When can the bot reply?#

Meta's pricing page explains: "When a WhatsApp user messages you or calls you, a 24-hour timer called a customer service window starts." It adds: "Businesses can only send non-template messages to users when this window is open." This bot answers within seconds, inside the window. To message a customer first, you need a template that Meta approves.

How much does a WhatsApp AI bot cost?#

There are two bills: the model's tokens, charged to your 5abeer balance, and Meta's message charges. In this example, the bot answers 4,000 messages a month, at about 1,200 tokens in and 250 out each:

Example: what Claude Sonnet 5.5 costs

4.8M input tokens and 1M output tokens

Our price$5.8870% below the official price
Official price$19.60

Official price from the provider's page, checked October 19, 2026.

The price per token for the default model:

Claude Sonnet 5.5 price per 1M tokens
Input$0.60Official price: $2
Output$3Official price: $10

Official price from the provider's page, checked October 19, 2026.

Meta's pricing page, effective October 1, 2026, says:

  • "Meta does not charge for messages from a WhatsApp user to a business."
  • "Meta provides every business phone number with a free tier of 1,000 service messages per month." (Pricing, checked 2026-11-01)
  • Beyond that, Meta charges per service message, at the rates on its page, except "for eligible government and non-profit organizations".

Each part of a long answer counts as a message. To spend less on the model, pick a smaller one from the models list, keep SYSTEM short or lower MAX_TURNS and max_tokens. Compare providers on the API page and the Claude, OpenAI and Gemini API pages, and plans on the pricing page.

Give the bot its own key with a spend limit, as in Key controls. At the limit, requests fail with API_KEY_QUOTA_EXHAUSTED and customers get the apology, so a busy day or abuse can't spend more than you set.

How do you keep the bot safe?#

  • Keep the 5abeer key, the WhatsApp token and the App Secret in environment variables on the server, never in code or a repository. If one leaks, replace it.
  • Keep the signature check. Without it, anyone who finds the address can send fake messages and spend your balance.
  • If the server has a fixed address, add an IP allow-list to its key, as in Key controls.
  • Keep secrets and other customers' data out of SYSTEM, since customers can try to talk the assistant out of its instructions.
  • Tell customers that an AI assistant answers them, and give them a way to reach a person.

FAQ#

Can I use a WhatsApp Web library instead of the Cloud API?#

No. Those libraries automate an ordinary account, which WhatsApp's terms forbid, and the number can be suspended.

Can the bot use GPT or Gemini instead of Claude?#

Yes. Set CHAT_MODEL to a model id from the models list and use a key for that model's provider. Each key covers one provider, so a GPT model with a Claude key returns MODEL_PLATFORM_NOT_ALLOWED.

Can it answer voice notes or images?#

Not as written. To add them, download the media through the Cloud API and send it to a model that accepts that input, or turn the voice note into text first.

What happens when my balance runs out?#

Requests fail with INSUFFICIENT_BALANCE, and customers get the apology until you top up. Meta's charges are separate, on your Meta account.

How do I pay for the API?#

You top up your 5abeer balance with any method in Payment methods, and the bot's API calls draw from that balance.

Is 5abeer part of Meta, OpenAI, Anthropic or Google?#

No. 5abeer is an independent service, not affiliated with any of them.

Sources#

Checked on 2026-11-01:

Get started#

Create a Claude key, put it in KHABEER_API_KEY and connect your WhatsApp number.