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
  1. Pick a trigger. A keyword (MENU, HI; exact match, any case) or the first message from a new contact.
  2. Add a menu step. Three choices or fewer: buttons. More: a menu list.
  3. 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.
  4. Test in the phone preview in the editor, which runs the same logic as the live bot.
  5. 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.

  • 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 free