← Blog

Claude API WhatsApp Chatbot Integration Guide

2026-10-22 · 5 min read · SubToAPI Team

Building a WhatsApp chatbot powered by Claude means connecting three pieces: the WhatsApp Business Platform (Cloud API), a webhook server you control, and calls to Claude's messages endpoint. There's no plugin that does this end-to-end — you write a small service that receives inbound WhatsApp messages, forwards the text to Claude, and sends the response back through the WhatsApp API. This guide walks through that architecture with working code.

The core challenge isn't calling Claude — that's a single HTTP request. It's everything around it: verifying WhatsApp webhook signatures, tracking per-user conversation history (WhatsApp gives you a phone number, not a session), respecting the 24-hour messaging window, and keeping your Claude API key safe on a server that's reachable from the public internet. We'll cover all of that.

Architecture overview

A typical setup looks like this:

  1. WhatsApp Cloud API (via Meta) sends inbound messages to a webhook URL you host.
  2. Your webhook server validates the request, extracts the message and sender's phone number, and looks up or creates a conversation history for that number.
  3. The server calls Claude with the conversation history and the new message.
  4. The reply is sent back to the user via the WhatsApp Graph API (POST /messages).
  5. Conversation state (usually just the last N turns) is stored in Redis, Postgres, or even an in-memory map for prototyping.

None of this requires a dedicated "WhatsApp SDK" for Claude — it's two REST APIs glued together by your own logic.

Setting up the webhook

You'll need a Meta developer app with WhatsApp enabled, a verified phone number, and a permanent access token. Meta sends a GET request to verify your webhook, then POSTs JSON payloads for every inbound message.

import express from "express";
const app = express();
app.use(express.json());

const VERIFY_TOKEN = process.env.WA_VERIFY_TOKEN;

// Webhook verification (Meta calls this once on setup)
app.get("/webhook", (req, res) => {
  const mode = req.query["hub.mode"];
  const token = req.query["hub.verify_token"];
  const challenge = req.query["hub.challenge"];

  if (mode === "subscribe" && token === VERIFY_TOKEN) {
    res.status(200).send(challenge);
  } else {
    res.sendStatus(403);
  }
});

app.listen(3000, () => console.log("Webhook listening on :3000"));

Handling inbound messages

Each POST from WhatsApp contains the sender's phone number and message text buried in a nested payload. Extract it, fetch the user's conversation history, and call Claude.

import fetch from "node-fetch";

const conversations = new Map(); // phone -> message history (use Redis in production)

app.post("/webhook", async (req, res) => {
  res.sendStatus(200); // acknowledge immediately, Meta retries on timeout

  const entry = req.body.entry?.[0]?.changes?.[0]?.value;
  const message = entry?.messages?.[0];
  if (!message || message.type !== "text") return;

  const from = message.from; // user's phone number
  const text = message.text.body;

  const history = conversations.get(from) || [];
  history.push({ role: "user", content: text });

  const reply = await callClaude(history);
  history.push({ role: "assistant", content: reply });
  conversations.set(from, history.slice(-20)); // keep last 20 turns

  await sendWhatsAppMessage(from, reply);
});

Calling Claude

You can call Claude directly with an Anthropic key, or through SubToAPI if you'd rather issue scoped sub_live_... keys per app or per team member and get usage metadata without building your own billing layer. Either way the request shape is the same style of call — here's an example using SubToAPI's endpoint:

async function callClaude(history) {
  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 500,
      system: "You are a concise, friendly WhatsApp support assistant. Keep replies short.",
      messages: history,
    }),
  });
  const data = await res.json();
  return data.content[0].text;
}

Keeping replies short matters here — WhatsApp is a chat interface, not a document viewer. Set max_tokens conservatively and instruct Claude in the system prompt to avoid markdown formatting, since WhatsApp only supports limited rich text (bold, _italic_).

Sending the reply back

async function sendWhatsAppMessage(to, text) {
  await fetch(
    `https://graph.facebook.com/v19.0/${process.env.WA_PHONE_ID}/messages`,
    {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.WA_ACCESS_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        messaging_product: "whatsapp",
        to,
        type: "text",
        text: { body: text },
      }),
    }
  );
}

Things that will bite you in production

The 24-hour window. WhatsApp only lets you send free-form messages within 24 hours of the user's last message. Outside that window you must use pre-approved template messages. Design your bot assuming this constraint — don't build flows that expect to message users out of the blue.

Webhook timeouts. Meta expects a 200 response quickly. If Claude's response takes a few seconds, acknowledge the webhook immediately (as shown above) and send the WhatsApp reply asynchronously once Claude responds.

Conversation history growth. Trimming to the last 10–20 turns keeps token usage predictable. For longer-running support threads, summarize older turns into a single system-context block rather than sending the full transcript every time.

Tool use for real actions. If your bot needs to look up orders, check inventory, or create tickets, use Claude's tool calling rather than parsing free text yourself — see /docs/tools for the request format.

Streaming doesn't help here. WhatsApp messages are sent as complete units, not token-by-token, so there's no benefit to streaming responses for this use case — just wait for the full completion and send once.

If you're running this for a small team or client project, /docs/quickstart covers getting a key and your first request working in a few minutes, and /docs/messages documents the full request and response schema you'll be working against above.

Scaling beyond a prototype

Once you have real traffic, move conversation state out of memory and into Redis or Postgres keyed by phone number, add a queue (SQS, BullMQ) between webhook receipt and Claude calls so bursts don't overwhelm you, and log request IDs so you can trace a WhatsApp message back to the exact Claude call that produced it. If multiple people on your team are building or maintaining the bot, per-person API keys with usage visibility make debugging and cost attribution much easier — see /pricing for how seat-based plans work.

FAQs

Do I need Meta's official WhatsApp Business API, or can I use a third-party wrapper? You need access to the WhatsApp Cloud API (via a Meta developer app) or an approved BSP (Business Solution Provider). Third-party libraries just wrap the same underlying Graph API calls — they don't change the architecture described here.

Can Claude handle multiple languages in a WhatsApp bot? Yes. Claude responds in whatever language the input is written in by default; if you want to force a specific language regardless of input, state that explicitly in the system prompt.

How do I keep API costs predictable for a high-volume WhatsApp bot? Cap max_tokens per reply, trim conversation history aggressively, and monitor usage per sender if you're serving multiple clients. If you're using SubToAPI, the dashboard gives you usage metadata per key so you can spot runaway conversations early.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →