When a user interacts with a buttons message or a list menu sent by your workspace, Wazapin triggers a webhook event.
Follow this guide to identify interactive replies and extract the exact choice the customer made.
When this matters
Interactive replies are critical for:
- Routing customers to different support flows (e.g. Sales vs Support).
- Confirming transaction actions (e.g., tapping “Confirm Order” or “Reschedule”).
- Implementing automated chatbot menus and FAQ navigators.
Processing flow
Handling an interactive choice involves three steps:
graph TD
A[User taps button or list item] --> B[Webhook message.new msg_type: interactive]
B --> C[GET /v1/messages/{id} to load message details]
C --> D{interactive.type?}
D -->|button_reply| E[Read button ID & title]
D -->|list_reply| F[Read row ID, title & description]1. Receive interactive webhook
Wazapin POSTs a lightweight message.new webhook containing msg_type: "interactive".
Here is an example webhook payload:
{
"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"conversation_id": "0f89b0f9-74b4-44f9-b9b6-48f6d4de57aa",
"contact_id": "c1a2b3c4-d5e6-7890-abcd-ef1234567890",
"channel_id": "wzp_abc123",
"direction": "inbound",
"from_phone": "6281234567890",
"msg_type": "interactive"
}2. Fetch selection details
Call GET /v1/messages/{messageID} using the message_id from the payload. The API response will contain an interactive object within the content block detailing the user’s input.
Interactive structures
Depending on the message type, the returned details will have one of two shapes:
Quick-reply buttons (button_reply)
If the user tapped a button, the interactive block will have a type of "button_reply":
{
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "interactive",
"content": {
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "confirm_yes",
"title": "Yes, I confirm"
}
}
}
}
}List menu row (list_reply)
If the user picked an option from a list menu, the interactive block will have a type of "list_reply":
{
"data": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"type": "interactive",
"content": {
"interactive": {
"type": "list_reply",
"list_reply": {
"id": "support_agent",
"title": "Talk to Support",
"description": "Connect to a live agent"
}
}
}
}
}Code example
Here is how to handle the webhook and process the user’s choice:
# Fetch the message details to check what choice the user made
curl -X GET "https://api.wazapin.com/v1/messages/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Accept: application/json"import express from "express";
import { Webhook } from "svix";
import { WazapinClient } from "@wazapin/sdk";
const app = express();
const wazapin = new WazapinClient({ apiKey: process.env.WAZAPIN_API_KEY });
const wh = new Webhook(process.env.WAZAPIN_WEBHOOK_SECRET!);
app.post("/webhooks/wazapin", express.raw({ type: "application/json" }), async (req, res) => {
try {
wh.verify(req.body, req.headers as Record<string, string>);
} catch (err) {
return res.status(403).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString("utf8"));
if (payload.direction === "inbound" && payload.msg_type === "interactive") {
const messageId = payload.message_id;
// 1. Retrieve the interactive choice
const { data: message } = await wazapin.messages.get(messageId);
const interactive = message.content?.interactive;
if (interactive) {
if (interactive.type === "button_reply") {
const button = interactive.button_reply;
console.log(`User tapped button ID: ${button.id} with label: ${button.title}`);
// Route customer to corresponding flow
} else if (interactive.type === "list_reply") {
const row = interactive.list_reply;
console.log(`User selected list option ID: ${row.id} with label: ${row.title}`);
// Handle list selection
}
}
}
res.status(200).send("OK");
});from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
import requests
import os
app = FastAPI()
wh = Webhook(os.environ["WAZAPIN_WEBHOOK_SECRET"])
API_KEY = os.environ["WAZAPIN_API_KEY"]
@app.post("/webhooks/wazapin")
async def handle_webhook(request: Request):
body = await request.body()
try:
wh.verify(body, dict(request.headers))
except WebhookVerificationError:
raise HTTPException(status_code=403, detail="Invalid signature")
payload = await request.json()
if payload.get("direction") == "inbound" and payload.get("msg_type") == "interactive":
message_id = payload.get("message_id")
# 1. Retrieve message details
response = requests.get(
f"https://api.wazapin.com/v1/messages/{message_id}",
headers={"X-Api-Key": API_KEY, "Accept": "application/json"}
)
if response.status_code == 200:
message_data = response.json().get("data", {})
interactive = message_data.get("content", {}).get("interactive", {})
interactive_type = interactive.get("type")
if interactive_type == "button_reply":
button = interactive.get("button_reply", {})
print(f"User tapped button: {button.get('id')} ({button.get('title')})")
elif interactive_type == "list_reply":
row = interactive.get("list_reply", {})
print(f"User selected option: {row.get('id')} ({row.get('title')})")
return {"ok": True}Troubleshooting
Webhook arrives but interactive is empty
Ensure you query the message by ID via GET /v1/messages/{messageID}. Webhook bodies are intentionally slimmed down and do not include the interactive reply block inline.
User pressed multiple buttons
WhatsApp does not allow multiple choice selections on a single message. Tap events are atomic and arrive as individual webhooks. If a user tries to tap buttons on older messages, your system will receive a new webhook event with a unique message_id. You can track the original message ID via the conversation timeline or message parent headers if applicable.