Documentation Index Fetch the complete documentation index at: https://docs.gateway.connexease.com/llms.txt
Use this file to discover all available pages before exploring further.
When a user sends a message to your WhatsApp number, or when the delivery status of an outbound message changes, the Connexease Gateway forwards that event to your server as an HTTP POST request. This page explains how to register your endpoint, what each event payload looks like, and how to handle retries and failure scenarios correctly.
How It Works
Meta sends a webhook to the Connexease Gateway.
The Gateway verifies the signature and forwards the event to your registered webhook URL.
Your server must return HTTP 200 . Heavy processing must be done asynchronously.
Setup
Step 1 — Register Your Webhook URL
Dashboard → Settings → Webhooks → enter your URL and set a webhook secret.
Step 2 — Choose Your Events
Select the events your application should receive:
Event Name Trigger messagesA user sent a message to your WhatsApp number message_statusOutbound message status changed (sent, delivered, failed) readA user read your outbound message message_template_statusA template was approved, rejected, or paused accountAccount or phone number update
Step 3 — Prepare Your Endpoint
Your webhook endpoint must:
Be accessible over HTTPS with a valid SSL certificate
Return HTTP 200 within 5 seconds
Handle heavy operations asynchronously (background job, queue)
Node.js (Express)
Python (Flask)
Go
import express from 'express' ;
const app = express ();
app . use ( express . json ());
const WEBHOOK_SECRET = process . env . WEBHOOK_SECRET ;
app . post ( '/webhook' , ( req , res ) => {
// 1. Verify the request comes from Connexease Gateway
const auth = req . headers [ 'authorization' ];
if ( ! auth || auth !== `Bearer ${ WEBHOOK_SECRET } ` ) {
return res . status ( 401 ). send ( 'Unauthorized' );
}
// 2. Acknowledge immediately — never delay this
res . sendStatus ( 200 );
// 3. Process asynchronously
setImmediate (() => processWebhook ( req . body ));
});
function processWebhook ( body ) {
const changes = body ?. entry ?.[ 0 ]?. changes ?? [];
for ( const change of changes ) {
const value = change . value ;
for ( const msg of value ?. messages ?? []) {
handleIncomingMessage ( msg , value . contacts ?.[ 0 ], value . metadata );
}
for ( const status of value ?. statuses ?? []) {
handleStatusUpdate ( status );
}
}
}
function handleIncomingMessage ( msg , contact , metadata ) {
console . log ( `[ ${ msg . type . toUpperCase () } ] from ${ msg . from } ( ${ contact ?. profile ?. name } )` );
// Access msg.text / msg.image / msg.audio / msg.document / msg.interactive / msg.location
}
function handleStatusUpdate ( status ) {
console . log ( ` ${ status . id } → ${ status . status } ` );
if ( status . status === 'failed' ) {
console . error ( 'Delivery failed:' , status . errors );
}
}
app . listen ( 3000 );
Retry Policy
If your server returns a connection error or HTTP 5xx, the Dispatcher retries:
Attempt Wait before retry 1st attempt Immediate 2nd attempt After 1 second 3rd attempt After 2 seconds
After 3 failed attempts, the event is dropped and logged to ClickHouse with ProcessingStatus: ERROR.
HTTP 4xx responses are treated as permanent failures and are not retried . If you return 401 Unauthorized (e.g. wrong secret), the event is lost.
Incoming Message Payloads
Text Message
{
"object" : "whatsapp_business_account" ,
"entry" : [{
"id" : "WABA_ID_123456" ,
"changes" : [{
"field" : "messages" ,
"value" : {
"messaging_product" : "whatsapp" ,
"metadata" : {
"display_phone_number" : "905321234567" ,
"phone_number_id" : "PHONE_NUMBER_ID_123"
},
"contacts" : [{
"profile" : { "name" : "John Smith" },
"wa_id" : "905399876543"
}],
"messages" : [{
"from" : "905399876543" ,
"id" : "wamid.HBgNOTA1Mzk5ODc2NTQzFQIAERgSM0Y3RDg5NjREOUNBMkFFNEE3AA==" ,
"timestamp" : "1711900000" ,
"type" : "text" ,
"text" : { "body" : "Hello, I'd like to know the status of my order." }
}]
}
}]
}]
}
Field Reference:
Field Type Description entry[0].idstring WhatsApp Business Account (WABA) ID value.metadata.phone_number_idstring Meta ID of the phone number that received the message. Critical for routing. value.metadata.display_phone_numberstring Human-readable phone number value.contacts[0].wa_idstring Sender’s WhatsApp ID (phone number) value.contacts[0].profile.namestring Sender’s WhatsApp display name value.messages[0].fromstring Sender’s phone number value.messages[0].idstring Unique ID of the incoming message value.messages[0].timestampstring Unix timestamp (in seconds) value.messages[0].typestring text, image, audio, document, interactive, locationvalue.messages[0].text.bodystring Message content (for type: "text")
Image Message
{
"messages" : [{
"from" : "905399876543" ,
"id" : "wamid.xxx" ,
"timestamp" : "1711900050" ,
"type" : "image" ,
"image" : {
"caption" : "Product photo" ,
"mime_type" : "image/jpeg" ,
"sha256" : "abc123..." ,
"id" : "MEDIA_ID_789"
}
}]
}
Field Description image.idMedia ID. Use the /media/{id} endpoint to download the content. image.mime_typeFile type (image/jpeg, image/png) image.sha256Hash for file integrity verification image.captionCaption written by the user (optional)
Audio Message
{
"messages" : [{
"from" : "905399876543" ,
"id" : "wamid.xxx" ,
"timestamp" : "1711900100" ,
"type" : "audio" ,
"audio" : {
"mime_type" : "audio/ogg; codecs=opus" ,
"sha256" : "def456..." ,
"id" : "MEDIA_ID_456" ,
"voice" : true
}
}]
}
Field Description audio.idMedia ID for downloading the audio file audio.voicetrue = user sent a voice noteaudio.mime_typeaudio/ogg; codecs=opus, audio/mp4, etc.
Document Message
{
"messages" : [{
"from" : "905399876543" ,
"id" : "wamid.xxx" ,
"timestamp" : "1711900150" ,
"type" : "document" ,
"document" : {
"caption" : "Invoice" ,
"filename" : "invoice_march_2025.pdf" ,
"mime_type" : "application/pdf" ,
"sha256" : "ghi789..." ,
"id" : "MEDIA_ID_321"
}
}]
}
Received when a user taps one of the buttons you sent:
{
"messages" : [{
"from" : "905399876543" ,
"id" : "wamid.xxx" ,
"timestamp" : "1711900200" ,
"type" : "interactive" ,
"interactive" : {
"type" : "button_reply" ,
"button_reply" : {
"id" : "home_delivery" ,
"title" : "Home Delivery"
}
}
}]
}
Field Description interactive.button_reply.idThe reply.id you set when sending the button message interactive.button_reply.titleThe button label shown to the user
Interactive — List Reply
Received when a user selects an item from a list you sent:
{
"messages" : [{
"from" : "905399876543" ,
"id" : "wamid.xxx" ,
"timestamp" : "1711900250" ,
"type" : "interactive" ,
"interactive" : {
"type" : "list_reply" ,
"list_reply" : {
"id" : "track_order" ,
"title" : "Track Order" ,
"description" : "Find out where your order is"
}
}
}]
}
Location Message
{
"messages" : [{
"from" : "905399876543" ,
"id" : "wamid.xxx" ,
"timestamp" : "1711900300" ,
"type" : "location" ,
"location" : {
"latitude" : 41.0082376 ,
"longitude" : 28.9783589 ,
"name" : "Taksim Square" ,
"address" : "Taksim, Istanbul"
}
}]
}
Status Update Payloads
Sent
{
"statuses" : [{
"id" : "wamid.HBgNOTA1Mzk5ODc2NTQzFQIAERgSM0Y3RDg5NjREOUNBMkFFNEE3AA==" ,
"status" : "sent" ,
"timestamp" : "1711900400" ,
"recipient_id" : "905399876543" ,
"conversation" : {
"id" : "CONV_ID_111" ,
"origin" : { "type" : "utility" }
},
"pricing" : {
"billable" : true ,
"pricing_model" : "CBP" ,
"category" : "utility"
}
}]
}
Delivered
The Gateway automatically deducts the Meta fee when status: "delivered" and pricing.billable: true.
{
"statuses" : [{
"id" : "wamid.HBgNOTA1Mzk5ODc2NTQzFQIAERgSM0Y3RDg5NjREOUNBMkFFNEE3AA==" ,
"status" : "delivered" ,
"timestamp" : "1711900450" ,
"recipient_id" : "905399876543" ,
"conversation" : {
"id" : "CONV_ID_111" ,
"origin" : { "type" : "utility" }
},
"pricing" : {
"billable" : true ,
"pricing_model" : "CBP" ,
"category" : "utility"
}
}]
}
Status Update Field Reference:
Field Type Description statuses[0].idstring The wamid of the message you sent — match with the stored ID statuses[0].statusstring sent, delivered, read, failedstatuses[0].timestampstring Unix timestamp of the status change statuses[0].recipient_idstring Recipient’s phone number statuses[0].conversation.idstring Meta conversation ID statuses[0].conversation.origin.typestring Conversation category: marketing, utility, authentication, service statuses[0].pricing.billablebool true = Meta fee will be deducted, false = free tierstatuses[0].pricing.categorystring Billing category
Read
{
"statuses" : [{
"id" : "wamid.HBgNOTA1Mzk5ODc2NTQzFQIAERgSM0Y3RDg5NjREOUNBMkFFNEE3AA==" ,
"status" : "read" ,
"timestamp" : "1711900500" ,
"recipient_id" : "905399876543"
}]
}
Failed
{
"statuses" : [{
"id" : "wamid.xxx" ,
"status" : "failed" ,
"timestamp" : "1711900550" ,
"recipient_id" : "905399876543" ,
"errors" : [{
"code" : 131047 ,
"title" : "Re-engagement message" ,
"message" : "Message failed to send because more than 24 hours have passed since the customer last replied to this number." ,
"error_data" : {
"details" : "The recipient phone is not reachable or has not opted in."
}
}]
}]
}
Field Description errors[0].codeMeta error code (full list: Meta Docs) errors[0].titleShort error title errors[0].messageDetailed description
System Event Payloads
Template Status Update
{
"entry" : [{
"id" : "WABA_ID_123456" ,
"changes" : [{
"field" : "message_template_status_update" ,
"value" : {
"event" : "APPROVED" ,
"message_template_id" : 1234567890 ,
"message_template_name" : "order_confirmation" ,
"message_template_language" : "en_US" ,
"reason" : null
}
}]
}]
}
event ValueDescription APPROVEDTemplate is ready to use REJECTEDTemplate was rejected — check the reason field PENDING_DELETIONTemplate is scheduled for deletion FLAGGEDTemplate was flagged by Meta PAUSEDPaused due to low quality score
Account Update
{
"entry" : [{
"id" : "WABA_ID_123456" ,
"changes" : [{
"field" : "account_update" ,
"value" : {
"phone_number" : "905321234567" ,
"event" : "ACCOUNT_UPDATE" ,
"ban_info" : {
"waba_ban_state" : "SCHEDULE_FOR_DISABLE" ,
"waba_ban_date" : "2025-05-01"
}
}
}]
}]
}
Key Notes
No delivery order guarantee. sent and delivered events may not arrive in order. Sort by statuses[0].timestamp.
Idempotency: Network issues or the retry mechanism may deliver the same wamid more than once. Make your processing logic idempotent (safe to run multiple times for the same ID).
Quick test: Messages sent with is_fake=true do not generate delivered webhooks. Use a real number or Meta’s test phone numbers to test the full webhook flow.
Checklist
Is your HTTPS endpoint publicly accessible?
Must have a valid SSL certificate and be reachable from the internet.
Are you returning HTTP 200 within 5 seconds?
Move DB writes, API calls, and emails to a background job.
Are you validating the Authorization header?
Without the Bearer WEBHOOK_SECRET check, you’re open to forged events.
Is your processing idempotent?
Ensure the same wamid cannot trigger duplicate side effects.
Have you subscribed to the right events?
At minimum, messages and message_status should be active in the Dashboard.