Skip to content

Buttons and lists

A fourth kind of send, alongside template, text and media — exactly one of the four per request. Tappable replies mean nobody has to type 1 for sales.

These are free-form messages, so the 24-hour window applies. Buttons do not reopen a closed one; only a template does.

Up to three, with titles of 20 characters. Every title in one message must be different, which this API checks before Meta does.

Terminal window
curl -X POST "https://vaarto.com/api/v1/messages/whatsapp" \
-H "Authorization: Bearer $VAARTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+919876543210",
"interactive": {
"type": "button",
"header": "Order #1029",
"body": "Your order is ready. How would you like it?",
"footer": "Reply anytime",
"buttons": [
{
"id": "deliver",
"title": "Deliver"
},
{
"id": "pickup",
"title": "Collect"
}
]
}
}'

A list holds up to ten rows across all of its sections.

Terminal window
curl -X POST "https://vaarto.com/api/v1/messages/whatsapp" \
-H "Authorization: Bearer $VAARTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+919876543210",
"interactive": {
"type": "list",
"body": "Which size?",
"button": "View sizes",
"sections": [
{
"title": "Drinks",
"rows": [
{
"id": "sku_s",
"title": "Small",
"description": "250ml"
},
{
"id": "sku_l",
"title": "Large",
"description": "500ml"
}
]
}
]
}
}'

The tap comes back as an ordinary message.received webhook, with text set to the button’s label and details.replyId set to the id you chose.

Branch on the id, never on the text. The label is copy somebody may reword; the id is the contract.

// 1. Ask.
await fetch("https://vaarto.com/api/v1/messages/whatsapp", {
method: "POST",
headers: { Authorization: auth, "Content-Type": "application/json" },
body: JSON.stringify({
to: customer,
interactive: {
type: "button",
body: "Your order is ready. How would you like it?",
buttons: [
{ id: "deliver", title: "Deliver" },
{ id: "pickup", title: "Collect" },
],
},
}),
});
// 2. The tap arrives on your webhook as a normal message.received.
function onInbound(data) {
switch (data.details?.replyId) {
case "deliver": return arrangeDelivery(data.from);
case "pickup": return sendPickupCode(data.from);
default: return handleFreeText(data.text);
}
}

details is ours and metadata is yours. Nothing the platform knows about a message is ever written into metadata, so your own JSON reads back exactly as you sent it — even on a message carrying buttons.