Typebot on WhatsApp through the official API, with ZapClaw
In short: ZapClaw delivers each WhatsApp message to a small relay, the relay runs your published Typebot through Typebot's Chat API, and the bot's replies go back through ZapClaw. Messages travel on Meta's official Cloud API, and the number keeps working in the WhatsApp Business app on your phone. You keep the bot you built, and you need no Meta app of your own.
Connect your number in minutes and put the bot you already built on it, hosted by Typebot or self-hosted.
Business plan: US$ 29 per month, with unlimited numbers
Start for free See the relayWhat you need to connect
- The WhatsApp Business app on the number, version 2.24.17 or newer, in regular use.
- Two-factor authentication on the personal Facebook account that authorizes the connection.
- The number not connected to another provider. If it is, disconnect it there first.
- A computer to run the connection, with your phone at hand to confirm it in the app.
You do not need a company tax ID or a verified business to connect. The full checklist.
Why ZapClaw for a Typebot on WhatsApp
- The number stays in the WhatsApp Business app. Through Coexistence your team keeps answering in the app while the bot works the same number. Typebot's own WhatsApp integration asks you to bring your own Meta app, and its documentation says a number already used in the WhatsApp Business app cannot be used with it.
- No Meta app of your own. You connect the number in Meta's Embedded Signup window, from the ZapClaw dashboard, and ZapClaw runs the Cloud API side.
- The bot you already built. The relay runs your published bot through Typebot's Chat API, so you keep building and publishing in Typebot as usual.
- No unofficial library behind your bot. Integrations that imitate WhatsApp Web go against WhatsApp's terms and put the number at risk. ZapClaw uses Meta's Cloud API, and Meta's policies apply as they do to any business on it.
- Deliveries you can check. Each event is signed with HMAC-SHA256 and gets up to 3 delivery attempts when your relay does not answer, and a failed event can be replayed from the dashboard.
Why a relay sits in between
Typebot cannot take ZapClaw's webhook as it is. Its Webhook block only resumes a conversation that is already paused on it, and its Chat API expects one call per message, with a session kept for each person.
So a small relay does that bookkeeping, the way Typebot's guide for messaging apps describes it: it starts a session on a customer's first message, continues it on the next ones and sends the bot's text bubbles back through ZapClaw. It is under 100 lines of Node.js, with nothing to install.
Set it up in four steps
- In Typebot, publish your bot and copy its public ID (where to find it). Keep the flow text first: text bubbles, text inputs and buttons.
- In ZapClaw, connect your number, create an API key and copy the Signing secret from the Webhook page.
- Run the relay below with those three values on a server with a public HTTPS address, and paste that address under Webhook in ZapClaw. ZapClaw delivers only to a public HTTPS address.
- Press Send test event in ZapClaw: the relay answers
200and starts nothing. Then send a WhatsApp message to your number from another phone: the bot answers.
The relay, ready to run
Save it as relay.mjs and start it with node relay.mjs on Node.js 18 or newer. It reads its settings from the environment: ZAPCLAW_API_KEY, ZAPCLAW_SECRET (the signing secret), TYPEBOT_PUBLIC_ID, TYPEBOT_API for a self-hosted Typebot, and PORT, the port the relay listens on (8787 by default), which your HTTPS proxy forwards to.
// relay.mjs: ZapClaw webhook in, Typebot Chat API, replies out through ZapClaw.
// Node.js 18 or newer, nothing to install. Start it with: node relay.mjs
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
const ZAPCLAW_API_KEY = process.env.ZAPCLAW_API_KEY; // zc_live_...
const ZAPCLAW_SECRET = process.env.ZAPCLAW_SECRET; // the webhook signing secret
const TYPEBOT_PUBLIC_ID = process.env.TYPEBOT_PUBLIC_ID; // your published bot
const TYPEBOT_API = process.env.TYPEBOT_API || 'https://typebot.co/api';
const sessions = new Map(); // customer: { id, choices }. Use your database in production.
const queues = new Map(); // one message at a time per customer, in order
const isSigned = (raw, header = '') => {
const hmac = createHmac('sha256', ZAPCLAW_SECRET).update(raw).digest('hex');
const expected = Buffer.from(`sha256=${hmac}`);
const given = Buffer.from(String(header));
return given.length === expected.length && timingSafeEqual(given, expected);
};
const typebot = async (path, body) => {
const res = await fetch(`${TYPEBOT_API}/v1/${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...body, textBubbleContentFormat: 'markdown' }),
});
return res.ok ? res.json() : null;
};
const sendText = (from, to, text) =>
fetch('https://api.zapclaw.app/api/v1/send/text', {
method: 'POST',
headers: { 'X-API-Key': ZAPCLAW_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ from, to, text }),
});
const answer = async ({ data, account }) => {
const customer = data.user_id || data.from;
const from = account.display_phone_number || account.phone_number_id;
const to = data.from || data.user_id;
const session = sessions.get(customer);
let reply = null;
if (session) {
// Typebot takes a button by its exact label, so accept its number or any letter case.
const typed = data.text.body.trim();
const choice = session.choices.find(
(label, i) => typed === String(i + 1) || typed.toLowerCase() === label.toLowerCase()
);
reply = await typebot(`sessions/${session.id}/continueChat`, {
message: { type: 'text', text: choice || typed },
});
}
if (!reply) {
// First message, or a session that expired (continueChat answers 404): start the bot.
reply = await typebot(`typebots/${TYPEBOT_PUBLIC_ID}/startChat`, {
prefilledVariables: { name: data.contact_name, phone: to },
});
if (!reply) return;
}
const choices =
reply.input?.type === 'choice input'
? reply.input.items.map((item) => item.content).filter(Boolean)
: [];
if (reply.input) sessions.set(customer, { id: reply.sessionId || session.id, choices });
else sessions.delete(customer); // the flow ended: the next message starts it again
const texts = (reply.messages || [])
.filter((m) => m.type === 'text')
// Typebot writes standard Markdown; WhatsApp reads *bold*, ~strike~ and bare links.
.map((m) => m.content.markdown
.replace(/\*\*(.+?)\*\*/g, (_, t) => `*${t}*`)
.replace(/~~(.+?)~~/g, (_, t) => `~${t}~`)
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => `${label} (${url})`));
if (choices.length) texts.push(choices.map((label, i) => `${i + 1}. ${label}`).join('\n'));
for (const text of texts) if (text) await sendText(from, to, text);
};
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const raw = Buffer.concat(chunks);
if (!isSigned(raw, req.headers['x-zapclaw-signature'])) return res.writeHead(401).end();
res.writeHead(200).end(); // answer ZapClaw now: it waits 10 seconds, a bot can take longer
let event;
try {
event = JSON.parse(raw.toString('utf8'));
} catch {
return;
}
if (event.event !== 'message.received' || event.data?.type !== 'text') return;
const key = event.data.user_id || event.data.from;
const next = (queues.get(key) || Promise.resolve())
.then(() => answer(event))
.catch((err) => console.error(err));
queues.set(key, next);
});
}).listen(process.env.PORT || 8787);
On a self-hosted Typebot, set TYPEBOT_API to the address of your viewer, the app where people answer your bots, followed by /api.
What it does: it checks the X-ZapClaw-Signature header, answers ZapClaw at once (ZapClaw waits 10 seconds, and a bot that calls an AI can take longer) and handles each customer's messages one at a time, in order. Options from a Buttons block go out as a numbered list, and the customer answers with the number or the label, in any letter case. When the flow ends, the next message starts it again.
Create variables named name and phone in your bot and the relay fills them on the first message with the contact's WhatsApp name and number. Sessions live in the relay's memory, so a restart starts every conversation over: in production, store them in your database, as Typebot's guide recommends.
The relay sends text bubbles. An image bubble carries its address in content.url, which you can send with POST /api/v1/send/media.
Calling ZapClaw from inside the bot
Typebot's HTTP Request block can call ZapClaw's API too, with an X-API-Key header and a custom body built from your variables: for example, to alert a salesperson on WhatsApp when a lead reaches the end of the bot. The same call as a curl command:
curl -X POST https://api.zapclaw.app/api/v1/send/text \
-H "X-API-Key: zc_live_..." \
-H "Content-Type: application/json" \
-d '{ "from": "15551617409", "to": "15555550100", "text": "New lead from the bot" }'
from is your connected number, which the webhook brings in account.display_phone_number, and to is the person you write to. A sent message answers 201. Free text works within 24 hours of that person's last message to your number; outside that window Meta accepts only an approved template, through POST /api/v1/send/template.
What reaches the relay
Each message comes as a message.received event: the text in data.text.body, the sender in data.from (or data.user_id for a contact who shows only a username), the contact's name in data.contact_name and your number in account.display_phone_number.
{
"event": "message.received",
"sequence": 1042,
"timestamp": "2026-10-15T14:00:00.000Z",
"data": {
"from": "15555550100",
"user_id": "US.1A2B3C4D5E6F7G8H9I0J",
"contact_name": "Maria Silva",
"message_id": "wamid.HBg...",
"type": "text",
"timestamp": "1792072800",
"text": { "body": "Hi" }
},
"account": {
"phone_number_id": "964283400112599",
"display_name": "My Business",
"display_phone_number": "+1 555 161-7409"
}
}
The relay passes on only incoming text messages: the dashboard's test event and every other event get a 200 and nothing more. Every event and field is in the docs.
What it costs
- Free: 500 messages a month and 1 number, with no card and no end date. At the limit, sending pauses and new messages are held until you subscribe.
- Business: US$ 29 per month, with 20,000 messages included and unlimited numbers.
- Agency: US$ 49 per month for agencies, with 3 clients and 60,000 messages included and unlimited numbers.
Meta bills its WhatsApp message fees to you directly, with no ZapClaw markup. Every plan and price.
Who it is for
Teams that built lead capture, qualification, FAQ or booking flows in Typebot and want them on the WhatsApp number their customers already use. Agencies connect their clients' numbers on the Agency plan, each client isolated from the others. It does not fit bulk broadcasting.
Go further
- Flowise on WhatsApp, when the answers should come from an AI agent rather than a scripted flow.
- WhatsApp in n8n, with a flow you import instead of a script.
- The official WhatsApp API with ZapClaw: what you get, the price and the requirements.
- The webhook reference, with every event and field.
Sources
Typebot's documentation checked on 15 October 2026. Features change, so check each source before you build.
- Typebot, integrate with external messaging apps via HTTP API: the relay pattern, one session per person and the 404 of an expired session.
- Typebot API reference, Start chat and Continue chat: the request and response fields.
- Typebot, Webhook block and HTTP Request block.
- Typebot, WhatsApp: its own integration, the Meta app it asks for and the WhatsApp Business app number.
- Typebot, variables (prefilled variables) and self-hosting configuration (the viewer address).
- Typebot source on GitHub: how a reply to a Buttons block is matched.
- ZapClaw: webhooks and signature verification in the docs, and what you need to connect.
Put your Typebot on WhatsApp today
Start on the free plan: one number, no card. Move to the Business plan when you need more numbers or more messages.
Start for free Read the webhook docs