React to button taps
هذا المحتوى غير متوفر بلغتك بعد.
A template with quick-reply buttons is a question: confirm the delivery, approve the request, yes or no. The answer comes back as a WhatsApp message, and tying that message to the question it answers is the part that usually goes wrong. This flow turns it into one event that names the button and the message it replies to.
-
Send the question with a payload on each button
Section titled “Send the question with a payload on each button”const res = await fetch('https://k-message.kerneltics.com/v1/messages', {method: 'POST',headers: {Authorization: `Bearer ${process.env.KM_API_KEY}`,'Content-Type': 'application/json','Idempotency-Key': `order-${order.id}-confirm`,},body: JSON.stringify({to: order.customerPhone,type: 'template',reference: `order:${order.id}`,bot_reply: false,template: {name: 'delivery_confirmation',parameters: { name: order.customerName, order: String(order.id) },button_parameters: { '0': 'confirm', '1': 'reschedule' },},}),})const message = await res.json()referenceis your own id, up to 255 characters, and comes back untouched when a button is tapped.button_parametersgives each quick-reply button a payload by its position on the template: the contact sees the button’s text, you get the payload.bot_reply: falsekeeps the chatbot out of the answer, which is what you want when your system is the one replying.message.idcomes back asin_reply_to.message_id, so store it if thereferencealone is not enough to find the question again. -
Subscribe to
Section titled “Subscribe to button.reply”button.replyTerminal window curl https://k-message.kerneltics.com/v1/webhooks \-H "Authorization: Bearer km_live_YOUR_KEY" \-H "Content-Type: application/json" \-d '{"name": "Button taps","url": "https://api.example.com/hooks/kmessage","events": ["button.reply"]}'Store the
secretfrom the response; it is shown once. A webhook you already have can takebutton.replyin itseventsthroughPUT /v1/webhooks/{id}. -
Handle the tap
Section titled “Handle the tap”The event carries what was tapped and which message it answers:
{"event": "button.reply","timestamp": "2026-08-20T09:20:11.804Z","data": {"message_id": "9a8b7c6d-...","contact_id": "3f2a...","contact_phone": "+966500000000","contact_name": "Sara","whatsapp_account": "Main","button": { "id": "confirm", "text": "Yes, confirm" },"in_reply_to": {"message_id": "6f1c2d3e-...","whatsapp_message_id": "wamid.HBgMOTY2NTAwMDAwMDAwFQIAERgS...","template_name": "delivery_confirmation","reference": "order:1001","sent_at": "2026-08-20T09:15:00Z"}}}case 'button.reply': {const ref = data.in_reply_to?.reference ?? ''if (!ref.startsWith('order:')) break // not one of oursconst order = await orders.get(ref.slice('order:'.length))if (!order || order.confirmedAt) break // already handled, or a retryif (data.button.id === 'confirm') await orders.confirm(order.id)if (data.button.id === 'reschedule') await orders.flagForCall(order.id)await reply(data.contact_id, order)break}Three things this handler does on purpose. It checks
referencebefore anything else, because every tap in the account arrives here, including taps on templates it has never heard of. It branches onbutton.id, never onbutton.text, because the text is whatever the template says today and the id is the payload you chose. And it treats an already-confirmed order as done rather than as an error, which is what makes a retried delivery harmless.in_reply_toisnullwhen the tap cannot be tied to a message you sent through K-Message. Ignore those unless you have a use for a bare tap.Verify the signature and answer
200before doing any of this, as with every event. See Receive messages. -
Reply to the person
Section titled “Reply to the person”A tap is a message from the contact, so the 24-hour window is open and free text is allowed:
async function reply(contactId, order) {await fetch('https://k-message.kerneltics.com/v1/messages', {method: 'POST',headers: {Authorization: `Bearer ${process.env.KM_API_KEY}`,'Content-Type': 'application/json','Idempotency-Key': `order-${order.id}-confirm-reply`,},body: JSON.stringify({contact_id: contactId,type: 'text',text: { body: `Thank you. Order ${order.id} is confirmed for tomorrow.` },}),})}contact_idcomes straight from the event, so there is no phone number to look up or normalise.
Without a webhook
Section titled “Without a webhook”callback_url on the send delivers the same button.reply event to one URL,
for that message only:
{ "to": "+966500000000", "type": "template", "reference": "order:1001", "callback_url": "https://api.example.com/orders/1001/answer?token=9f8e7d6c", "template": { "name": "delivery_confirmation", "parameters": { "name": "Sara", "order": "1001" }, "button_parameters": { "0": "confirm", "1": "reschedule" } }}Nothing to register and nothing to subscribe to. The trade is that the
delivery is not signed: the URL is not a configured webhook and has no
secret. Put a token in the URL, as above, and treat whoever presents it the
way you would treat any bearer of that token. Retries follow the webhook
policy, three attempts with backoff on a 5xx or a timeout. Loopback,
private and link-local hosts are refused, as they are for webhooks.
Both can be in place at once: the organization’s webhooks get the event, and so does the URL.
Keeping the bot quiet
Section titled “Keeping the bot quiet”Without bot_reply: false, a tap reaches the chatbot like any other inbound
message, and a flow or an AI agent may answer it before your system does. Set
it when your system owns the reply. It applies to that one message: the tap on
it, and anything the contact types as a reply to it, skip automation. They are
still stored, still visible in the inbox, and still delivered to your
webhooks. The rest of the conversation is unaffected.
When the contact types instead
Section titled “When the contact types instead”Someone who types “yes” rather than tapping produces message.incoming, not
button.reply. Subscribe to both if you need to catch that, and match on
content as a fallback. The tap is the reliable path, and the reason to
prefer buttons over asking for a word.