wats.sh
Guides

Handle button and list replies

Send interactive buttons and lists, await the click or selection, and route inbound replies with typed filters.

active · reviewed 2026-07-05

sendButtons and sendList return waitable sent-results. The facade resolves waitForClick on an inbound button_reply and waitForSelection on a list_reply whose context.messageId matches the sent message id — no manual correlation, no polling.

import { createWhatsApp } from "@wats/core";

const wa = createWhatsApp({
  accessToken: process.env.WATS_ACCESS_TOKEN!,
  phoneNumberId: process.env.WATS_PHONE_NUMBER_ID!,
});

const sent = await wa.sendButtons({
  to: "<recipient-e164>",
  bodyText: "Pick a shipping speed",
  buttons: [
    { id: "standard", title: "Standard" },
    { id: "express", title: "Express" },
  ],
});

const click = await sent.waitForClick({ timeoutMs: 60_000 });
const choice = click.message.interactive.buttonReply.id; // "express"

For a list:

const list = await wa.sendList({
  to: "<recipient-e164>",
  bodyText: "Choose a slot",
  buttonText: "Slots",
  sections: [
    {
      rows: [
        { id: "mon-am", title: "Mon AM" },
        { id: "tue-pm", title: "Tue PM" },
      ],
    },
  ],
});

const pick = await list.waitForSelection({ timeoutMs: 60_000 });
const slot = pick.message.interactive.listReply.id; // "mon-am"

To route replies as they arrive (not as a waiter on a specific send), use typed filters. The interactiveButtonReply and interactiveListReply predicates narrow the update so the handler sees a typed reply:

import { message } from "@wats/core/filtersTyped";

wa.on(message.interactiveButtonReply(), (ctx) => {
  const id = ctx.update.message.interactive.buttonReply.id;
  // dispatch on id
});

The waiter and the router share the same listener substrate, so a click resolves a pending waitForClick and fires any matching on handler.

On this page