Skip to main content

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

  1. Meta sends a webhook to the Connexease Gateway.
  2. The Gateway verifies the signature and forwards the event to your registered webhook URL.
  3. Your server must return HTTP 200. Heavy processing must be done asynchronously.

Setup

Step 1 — Register Your Webhook URL

DashboardSettings → Webhooks → enter your URL and set a webhook secret.

Step 2 — Choose Your Events

Select the events your application should receive:
Event NameTrigger
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)
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:
AttemptWait before retry
1st attemptImmediate
2nd attemptAfter 1 second
3rd attemptAfter 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:
FieldTypeDescription
entry[0].idstringWhatsApp Business Account (WABA) ID
value.metadata.phone_number_idstringMeta ID of the phone number that received the message. Critical for routing.
value.metadata.display_phone_numberstringHuman-readable phone number
value.contacts[0].wa_idstringSender’s WhatsApp ID (phone number)
value.contacts[0].profile.namestringSender’s WhatsApp display name
value.messages[0].fromstringSender’s phone number
value.messages[0].idstringUnique ID of the incoming message
value.messages[0].timestampstringUnix timestamp (in seconds)
value.messages[0].typestringtext, image, audio, document, interactive, location
value.messages[0].text.bodystringMessage 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"
    }
  }]
}
FieldDescription
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
    }
  }]
}
FieldDescription
audio.idMedia ID for downloading the audio file
audio.voicetrue = user sent a voice note
audio.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"
    }
  }]
}

Interactive — Button Reply

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"
      }
    }
  }]
}
FieldDescription
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:
FieldTypeDescription
statuses[0].idstringThe wamid of the message you sent — match with the stored ID
statuses[0].statusstringsent, delivered, read, failed
statuses[0].timestampstringUnix timestamp of the status change
statuses[0].recipient_idstringRecipient’s phone number
statuses[0].conversation.idstringMeta conversation ID
statuses[0].conversation.origin.typestringConversation category: marketing, utility, authentication, service
statuses[0].pricing.billablebooltrue = Meta fee will be deducted, false = free tier
statuses[0].pricing.categorystringBilling 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."
      }
    }]
  }]
}
FieldDescription
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

1

Is your HTTPS endpoint publicly accessible?

Must have a valid SSL certificate and be reachable from the internet.
2

Are you returning HTTP 200 within 5 seconds?

Move DB writes, API calls, and emails to a background job.
3

Are you validating the Authorization header?

Without the Bearer WEBHOOK_SECRET check, you’re open to forged events.
4

Is your processing idempotent?

Ensure the same wamid cannot trigger duplicate side effects.
5

Have you subscribed to the right events?

At minimum, messages and message_status should be active in the Dashboard.