تخطَّ إلى المحتوى

Event reference

هذا المحتوى غير متوفر بلغتك بعد.

Every delivery has the same envelope:

{
"event": "message.delivered",
"timestamp": "2026-08-20T09:15:04.221Z",
"data": { }
}

And the same headers:

HeaderMeaning
X-KM-EventThe event name, so you can route before parsing
X-KM-Delivery-IdShared by every attempt at one event
X-KM-TimestampUnix seconds, part of the signed string
X-KM-Signaturev1=<hex>, see Verify webhook signatures
X-Webhook-SignatureThe 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.

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.

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).

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.

The recipient opened it. Not all recipients have read receipts enabled, so absence of this event means nothing.

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.

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"
}
}
FieldMeaning
message_idThe inbound message that recorded the tap
button.idThe 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.textThe title the contact saw
in_reply_to.message_idYour message that was answered, the id returned when you sent it
in_reply_to.whatsapp_message_idThe same message as Meta identifies it
in_reply_to.template_nameThe template it was sent from. Empty for a non-template message.
in_reply_to.referenceThe reference you sent with it. Empty if you sent none.
in_reply_to.sent_atWhen 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.

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"
}

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.

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"
}

An agent picked it up. Carries agent_id and agent_name.

The handover closed and the conversation went back to the chatbot.

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.

Meta approved a template and it can now be sent.

{
"template_id": "7c6b5a4d-...",
"template_name": "order_shipped",
"language": "ar",
"status": "APPROVED"
}

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.