← Blog

Claude API WhatsApp Bot Tutorial: Build It Step by Step

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

Building a WhatsApp bot with Claude means connecting two separate APIs: the WhatsApp Cloud API (which handles sending and receiving messages) and Claude's API (which generates the responses). There's no native "Claude for WhatsApp" integration — you write a small webhook server that sits between the two, forwarding incoming messages to Claude and relaying the replies back.

This tutorial walks through the full setup: registering a WhatsApp Business app, building the webhook, calling Claude for each incoming message, and handling things real bots need — session memory, typing delays, and rate limits. By the end you'll have a working bot you can message from your own phone.

What You Need Before Starting

Step 1: Set Up the WhatsApp Webhook

In the Meta developer dashboard, create a WhatsApp Business app and configure a webhook URL pointing to your server. WhatsApp sends a verification request (GET with a hub.challenge parameter) that your server must echo back:

app.get("/webhook", (req, res) => {
  const verifyToken = process.env.WA_VERIFY_TOKEN;
  if (req.query["hub.verify_token"] === verifyToken) {
    res.send(req.query["hub.challenge"]);
  } else {
    res.sendStatus(403);
  }
});

After verification, subscribe to the messages webhook field so incoming texts are POSTed to your server.

Step 2: Receive Incoming Messages

Incoming WhatsApp messages arrive as a nested JSON payload. Extract the sender's phone number and message text:

app.post("/webhook", async (req, res) => {
  const entry = req.body.entry?.[0]?.changes?.[0]?.value;
  const message = entry?.messages?.[0];
  if (!message) return res.sendStatus(200);

  const from = message.from;
  const text = message.text?.body;

  await handleIncoming(from, text);
  res.sendStatus(200);
});

Always return 200 quickly — WhatsApp retries if your webhook is slow or errors out, which can cause duplicate replies if you're not careful.

Step 3: Call Claude for a Response

This is where most of the actual "intelligence" lives. Send the user's message to Claude and get back a reply. Using SubToAPI, the request looks like a standard HTTPS call:

async function getClaudeReply(userText, 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",
      max_tokens: 500,
      system: "You are a friendly WhatsApp assistant. Keep replies short and conversational.",
      messages: [...history, { role: "user", content: userText }]
    })
  });
  const data = await res.json();
  return data.content[0].text;
}

See the messages docs for the full request and response schema. The system prompt matters more here than in a typical chatbot — WhatsApp users expect short, texting-style replies, not essay-length answers, so tell Claude that explicitly.

Step 4: Send the Reply Back to WhatsApp

Use the WhatsApp Cloud API's /messages endpoint to send the generated text back to the user:

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_TOKEN}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      messaging_product: "whatsapp",
      to,
      text: { body: text }
    })
  });
}

Wire the two together: on each incoming message, call getClaudeReply, then sendWhatsAppMessage with the result.

Step 5: Add Conversation Memory

WhatsApp doesn't track conversation state for you — each webhook call is stateless from Claude's perspective unless you pass history yourself. Keep a simple per-number message log in memory (Redis or a database in production):

const sessions = new Map();

function getHistory(phone) {
  return sessions.get(phone) || [];
}

function appendToHistory(phone, role, content) {
  const history = getHistory(phone);
  history.push({ role, content });
  sessions.set(phone, history.slice(-10)); // keep last 10 turns
}

Trim history aggressively — WhatsApp conversations tend to be short bursts, and sending unbounded history on every turn burns tokens for no benefit.

Step 6: Handle Media and Typing Indicators

WhatsApp messages can include images, voice notes, and documents, not just text. Check message.type and branch accordingly — for images, you'll need to download the media via the Cloud API's media endpoint before passing it to Claude as an attachment, since Claude's messages API accepts image content blocks directly.

For a more natural feel, send a "typing" presence update before your Claude call resolves, since generation can take a second or two and users expect some feedback.

Why Use a Gateway Instead of the Raw Anthropic API

You can call Anthropic's API directly from your webhook, but a gateway layer solves a few recurring problems: issuing separate sub_live_ keys per bot or environment so you can revoke one without affecting others, streaming support if you later move to a richer chat UI, and centralized usage tracking across multiple bots if you're running more than one WhatsApp number. Start with the quickstart to get a key, and check streaming if you want token-by-token responses for longer answers.

Common Pitfalls

Questions

Do I need Anthropic API access directly, or can I use a third-party gateway? Either works. Direct Anthropic access requires your own API key and billing; a gateway like SubToAPI gives you an HTTPS endpoint, scoped keys, and usage metadata without managing Anthropic billing separately — useful if you're running multiple bots or clients.

Can the bot handle images and voice notes sent via WhatsApp? Yes, but you need to download the media file from WhatsApp's Cloud API first, then pass it to Claude as an image content block. Voice notes need transcription before Claude can process them, since Claude doesn't accept raw audio input.

How do I keep the bot from losing context between messages? Store a short rolling history per phone number (Redis or a database) and include it in every request to Claude's messages endpoint. WhatsApp itself doesn't maintain conversation state — your server has to.

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 →