Event reference
هذا المحتوى غير متوفر بلغتك بعد.
Every delivery has the same envelope:
{ "event": "message.delivered", "timestamp": "2026-08-20T09:15:04.221Z", "data": { }}And the same headers:
| Header | Meaning |
|---|---|
X-KM-Event | The event name, so you can route before parsing |
X-KM-Delivery-Id | Shared by every attempt at one event |
X-KM-Timestamp | Unix seconds, part of the signed string |
X-KM-Signature | v1=<hex>, see Verify webhook signatures |
X-Webhook-Signature | The older scheme, still sent |
Reply 2xx as fast as you can and do the work afterwards. We retry three times
with backoff on a 5xx or a timeout, and do not retry a 4xx, which cannot
succeed by repetition.
Messages
Section titled “Messages”message.incoming
Section titled “message.incoming”A contact sent you a message.
{ "message_id": "6f1c2d3e-...", "contact_id": "3f2a...", "contact_phone": "+966500000000", "contact_name": "Sara", "message_type": "text", "content": "Has it shipped yet?", "whatsapp_account": "Main"}This also opens the 24-hour window for that contact, so it is the moment free text becomes possible again.
message.sent
Section titled “message.sent”A message left K-Message, whether from an agent, the chatbot or the API. It means we handed it to WhatsApp, not that anyone received it.
If you sent it through the API with a reference, the event carries it back as
reference (see below).
message.delivered
Section titled “message.delivered”WhatsApp confirmed the message reached the recipient’s device.
{ "message_id": "6f1c2d3e-...", "contact_id": "3f2a...", "contact_phone": "+966500000000", "wamid": "wamid.HBgMOTY2NTAwMDAwMDAwFQIAERgS...", "status": "delivered", "reference": "account.move:42"}This is the event to record against your own order or ticket. wamid is Meta’s
identifier, which is what you will be looking at if you ever compare our
records with your Meta dashboard.
reference is your own id for the message, exactly as you sent it in the
reference field of the send request (for example an invoice or order number).
It is on message.sent, message.delivered, message.read and
message.failed, and left out when you did not send one, so you can match the
event to your record without storing our message_id.
message.read
Section titled “message.read”The recipient opened it. Not all recipients have read receipts enabled, so absence of this event means nothing.
message.failed
Section titled “message.failed”WhatsApp refused the message. This fires in two situations: WhatsApp accepted
it and reported the failure later, or WhatsApp refused it straight away when we
handed it over. In the second case there is no wamid, because WhatsApp never
issued one, and this is the only event the message will ever produce.
{ "message_id": "6f1c2d3e-...", "contact_id": "3f2a...", "contact_phone": "+966500000000", "status": "failed", "error_message": "Message failed to send because more than 24 hours have passed since the customer last replied to this number", "reference": "account.move:42"}error_message is Meta’s explanation, in plain words where we know the cause.
The most common causes are being outside the 24-hour window, a template that is
no longer approved, and a number that is not on WhatsApp. When Meta cannot
charge the business’s WhatsApp account, the text (in Arabic and English) asks you to add or update a
payment method in the Meta Business settings, so you can show it to your team
as it is. A message that is refused at hand-over is not retried.
Buttons
Section titled “Buttons”button.reply
Section titled “button.reply”A contact tapped a button: a quick-reply button on a template, a button on an interactive message, or a row in a list. One small event naming what was tapped and which of your messages it answers, instead of the whole conversation.
{ "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" }}| Field | Meaning |
|---|---|
message_id | The inbound message that recorded the tap |
button.id | The button’s payload: what you passed in button_parameters for a template quick reply, or the button or row id of an interactive message. The button text when no payload was set. |
button.text | The title the contact saw |
in_reply_to.message_id | Your message that was answered, the id returned when you sent it |
in_reply_to.whatsapp_message_id | The same message as Meta identifies it |
in_reply_to.template_name | The template it was sent from. Empty for a non-template message. |
in_reply_to.reference | The reference you sent with it. Empty if you sent none. |
in_reply_to.sent_at | When it left K-Message |
in_reply_to is null when the tap cannot be tied to a message we sent:
WhatsApp did not say which message it answers, or that message went out
through something other than K-Message. Treat it as a bare tap, not an error.
Branch on button.id, never on button.text. The text is whatever the
template says today and changes the next time someone edits it; the id is
yours.
Fires once the tap is saved, so message_id is real, and regardless of what
the chatbot does next. message.incoming fires for the same tap too, with
message_type: "button_reply", so a system that wants only answers can
subscribe to button.reply alone.
Delivered to every webhook subscribed to it, signed like any other event, and
also to the callback_url set on the message being answered, if there was
one. That second delivery is not signed. See
React to button taps.
Contacts
Section titled “Contacts”contact.created
Section titled “contact.created”A contact was added, either by messaging you for the first time or through the API.
{ "contact_id": "3f2a...", "contact_phone": "+966500000000", "contact_name": "Sara", "whatsapp_account": "Main"}contact.updated
Section titled “contact.updated”A contact’s details or tags changed.
{ "contact_id": "3f2a...", "contact_phone": "+966500000000", "contact_name": "Sara Al-Ahmed", "tags": ["vip", "riyadh"]}Subscribe to both if you mirror contacts into your own system. A sync that heard only about creations would drift out of date on the first edit.
Handovers
Section titled “Handovers”transfer.created
Section titled “transfer.created”A conversation asked for a human agent, either because the customer requested one or because the chatbot escalated.
{ "transfer_id": "4e5f6a7b-...", "contact_id": "3f2a...", "contact_phone": "+966500000000", "contact_name": "Sara", "source": "chatbot", "reason": "Customer asked to speak to someone", "whatsapp_account": "Main"}transfer.assigned
Section titled “transfer.assigned”An agent picked it up. Carries agent_id and agent_name.
transfer.resumed
Section titled “transfer.resumed”The handover closed and the conversation went back to the chatbot.
Campaigns
Section titled “Campaigns”campaign.completed
Section titled “campaign.completed”A bulk send finished, with its settled counts.
{ "campaign_id": "5d4c3b2a-...", "campaign_name": "Eid offer", "status": "completed", "total_recipients": 1200, "sent": 1197, "delivered": 1102, "read": 640, "failed": 3, "skipped": 0}Fires once, when every recipient has been dealt with. A campaign parked against the daily allowance has not completed and does not fire this.
Templates
Section titled “Templates”template.approved
Section titled “template.approved”Meta approved a template and it can now be sent.
{ "template_id": "7c6b5a4d-...", "template_name": "order_shipped", "language": "ar", "status": "APPROVED"}template.rejected
Section titled “template.rejected”Meta rejected it, with the reason when one was given.
{ "template_id": "7c6b5a4d-...", "template_name": "order_shipped", "language": "ar", "status": "REJECTED", "reason": "INVALID_FORMAT"}Meta’s review is asynchronous and can take hours. These two events exist so a system that submits templates does not have to poll for the outcome.