WhatsApp chatbot menu with reply buttons and lists — limits, payloads and a no-code build
How WhatsApp reply buttons and list menus work (3 buttons, 10 rows, character limits), the raw Cloud API payloads, and how to build the same menu bot without code.
November 4, 2026 · 7 min read
A WhatsApp menu bot is a message with tappable choices: Pricing, Support, Talk to a person. People tap instead of typing, and your bot knows exactly what they chose. WhatsApp has two built-in formats for this, and each has hard limits. This guide covers both, shows the raw payloads for developers, and then the no-code way to build the same bot.
SendFromChat is an independent product and is not affiliated with Meta or WhatsApp. The limits below come from Meta's Cloud API documentation (interactive reply buttons and interactive list messages pages), read on 4 Nov 2026. Meta can change them, so check the pages before you ship.
Buttons or a list?
| Reply buttons | List message | |
|---|---|---|
| Choices | Up to 3 | Up to 10 rows in total |
| Choice label | Up to 20 characters | Row title up to 24 characters |
| Message text | Up to 1,024 characters | Up to 4,096 characters |
| Best for | Yes/no, "Change / Cancel", 2–3 main paths | A full menu: products, topics, opening hours |
A list also has one button that opens it; that button's label is limited to 20 characters. Lists can be split into sections, but the 10-row cap applies across all sections combined. Each section needs a title of up to 24 characters.
Rule of thumb: three choices or fewer, use buttons. More than three, use a list. If you need more than ten, split the menu in two levels ("Orders", "Returns", "Other"), where each choice opens a smaller menu.
When you are allowed to send them
Buttons and lists are free-form messages, so they follow the same rule as any free-form message: they can go out within 24 hours of the person's last message to you (the 24-hour window, explained). In practice a menu bot answers someone who just wrote "MENU" or "hi", so you are inside the window. Outside it, you need an approved template, and some templates can carry buttons of their own (template rules).
The raw payload (Cloud API)
If you call Meta's Cloud API yourself, a reply-buttons message looks like this. The field names follow Meta's documentation; the text is our own example.
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "+14155550132",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Your order ships tomorrow. What would you like to do?" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "change", "title": "Change address" } },
{ "type": "reply", "reply": { "id": "cancel", "title": "Cancel order" } },
{ "type": "reply", "reply": { "id": "person", "title": "Talk to a person" } }
]
}
}
}
When the person taps one, Meta's webhook sends an interactive object of type button_reply with the id and title you set. Use the id in your code, not the title, so you can rename a label later without breaking the logic.
A list message uses "type": "list" with a button label and rows:
{
"messaging_product": "whatsapp",
"to": "+14155550132",
"type": "interactive",
"interactive": {
"type": "list",
"body": { "text": "How can we help?" },
"action": {
"button": "Choose a topic",
"sections": [
{
"title": "Topics",
"rows": [
{ "id": "pricing", "title": "Pricing" },
{ "id": "support", "title": "Support" },
{ "id": "hours", "title": "Opening hours" }
]
}
]
}
}
}
Check the limits before you send
A message that breaks a limit may be rejected by Meta. This small Node function (no dependencies) checks a payload against the limits in the table above and returns a list of problems. Save it as check-menu.mjs.
// Limits as read on Meta's docs on 4 Nov 2026. Re-check them before relying on this.
export function checkInteractive(payload) {
const problems = [];
const i = payload.interactive;
if (!i) return ["missing interactive object"];
if (i.type === "button") {
const buttons = i.action?.buttons ?? [];
if ((i.body?.text ?? "").length > 1024) problems.push("body over 1,024 characters");
if (buttons.length === 0 || buttons.length > 3) problems.push("needs 1 to 3 buttons");
for (const b of buttons) {
if ((b.reply?.title ?? "").length > 20) problems.push(`button "${b.reply.title}" over 20 characters`);
}
} else if (i.type === "list") {
const sections = i.action?.sections ?? [];
const rows = sections.flatMap((s) => s.rows ?? []);
if ((i.body?.text ?? "").length > 4096) problems.push("body over 4,096 characters");
if ((i.action?.button ?? "").length > 20) problems.push("list button label over 20 characters");
if (rows.length === 0 || rows.length > 10) problems.push("needs 1 to 10 rows in total");
for (const r of rows) {
if ((r.title ?? "").length > 24) problems.push(`row "${r.title}" over 24 characters`);
}
} else {
problems.push(`unsupported type "${i.type}"`);
}
return problems;
}
// Example: every label in the buttons payload above is 20 characters or fewer, so this prints [].
const demo = {
interactive: {
type: "button",
body: { text: "Your order ships tomorrow. What would you like to do?" },
action: {
buttons: [
{ type: "reply", reply: { id: "change", title: "Change address" } },
{ type: "reply", reply: { id: "cancel", title: "Cancel order" } },
{ type: "reply", reply: { id: "person", title: "Talk to a person" } },
],
},
},
};
console.log(checkInteractive(demo));
Run it with node check-menu.mjs. It only validates the shape; it does not call Meta or send anything. Whether Meta accepts a given message is decided by Meta.
Build the same menu bot without code
In SendFromChat a menu bot is a chatbot flow: a set of steps with a trigger (flows reference).
| Step | What it does |
|---|---|
| Message | Sends text, then continues |
| Message with buttons | Text plus up to 3 reply buttons (labels up to 20 characters); waits for a tap |
| Menu list | Text plus a menu of up to 10 options; waits for a pick |
| Ask a question | Waits for any reply and saves it to the contact, for example as email |
| Hand over to a person | Sends text, ends the flow and flags the chat "Wants a person" in the Inbox |
- Pick a trigger. A keyword (MENU, HI; exact match, any case) or the first message from a new contact.
- Add a menu step. Three choices or fewer: buttons. More: a menu list.
- Point each choice somewhere. To a reply message, to a question ("What is your order number?") or to a hand-over to a person. A choice can also end the flow.
- Test in the phone preview in the editor, which runs the same logic as the live bot.
- Switch it on. New flows are saved switched off.
Or describe it in chat to your AI assistant: "Build a menu for MENU with Pricing, Support and Talk to a person; on Support ask for their order number." It drafts the flow, shows it to you, and saves it after you agree (connect your assistant).
How the bot behaves
- People can tap a choice or type its label or its number ("2").
- An answer that does not match re-asks the same step.
- Sending the trigger keyword again restarts the flow.
- A person replying from the Inbox, or the customer sending STOP, ends the flow for that chat.
- A flow left unanswered for 24 hours expires.
What it costs
Flow messages answer an incoming message, so they sit inside the 24-hour window. They do not count toward your SendFromChat monthly message allowance. On Meta's side they are service messages: the first 1,000 a month per business phone number are free, then Meta charges (Meta pricing page, checked 23 Oct 2026). More in WhatsApp API pricing.
Menu design tips
- Start with the real top three questions from your inbox, not everything you offer.
- Always include a way to reach a person. A bot that cannot hand over frustrates people fast.
- Keep labels short and concrete: "Track my order" beats "Order tracking services".
- Never loop plain messages. Flows that only send messages with nothing waiting for a reply are rejected when you save.
- Do not collect more than you need. If you save answers to contacts, handle them under your own consent and privacy rules (WhatsApp and GDPR).
Try it
Start the 30-day free trial (no card), connect a number, and build the menu in the editor. Related: auto-replies and the inbox, reply to incoming messages with webhooks and an AI reply assistant with Claude.
Do this from your AI assistant
SendFromChat connects Claude, ChatGPT, Cursor or any MCP client to the WhatsApp Business API — templates, bulk sends, inbox, chatbots and sequences, just by asking. 30 days free, no card.
Start freeMore guides
Send WhatsApp messages from PHP and Laravel: a working example
Send WhatsApp template messages from plain PHP (cURL) or Laravel's HTTP client with a REST API — test keys, safe retries, errors by type and signed webhook verification.
WhatsApp for Black Friday: a dated plan to be ready to send on 27 November 2026
Black Friday is Friday 27 November 2026. A step-by-step plan for a first WhatsApp campaign - connect a number, collect opt-in, get templates approved, warm up and send - with the limits that decide how many people you can reach.
Track WhatsApp delivery status in your app: sent, delivered, read and failed
Store the status of every WhatsApp message you send, handle receipts that arrive late or twice, read the failure reason, and fall back to polling when a webhook is missed. TypeScript example with a REST API.
Do you need a Facebook login for the WhatsApp Business API? What 'Continue with Facebook' asks
What the Facebook window asks you to do when you connect a WhatsApp number, which two permissions it gives an app, what you keep, and how to remove the access later. Based on Meta's own pages, read 31 Oct 2026.