Claude API WhatsApp Bot Tutorial: Build It Step by Step
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
- A Meta developer account with a WhatsApp Business app (free tier works for testing)
- A publicly reachable HTTPS endpoint for your webhook (ngrok is fine for development)
- A way to call Claude — either direct Anthropic API access or a hosted gateway like SubToAPI, which gives you a stable HTTPS endpoint and API key without managing your own Claude account credentials
- Node.js or Python for the webhook server
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
- Double replies: if your webhook handler is slow, WhatsApp retries the delivery — always ack with
200immediately and process asynchronously. - Rate limits: WhatsApp enforces messaging limits per phone number tier; batch-test carefully before going to production.
- Session leaks: without a TTL on your history map, long-idle users will resume stale conversations weeks later. Expire sessions after a few hours of inactivity.
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.