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

# Webhooks

> Receive incoming messages, delivery receipts, and system events in real time.

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

```mermaid theme={null}
sequenceDiagram
    participant M as Meta
    participant G as Connexease Gateway
    participant Y as Your Server

    M->>G: POST /v1/wa/webhook
    G->>G: Verify signature
    G->>Y: POST your_webhook_url (Authorization: Bearer secret)
    Y-->>G: HTTP 200
```

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

[Dashboard](https://gateway.connexease.com) → **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                                                   |
| ------------------------- | --------------------------------------------------------- |
| `messages`                | A user sent a message to your WhatsApp number             |
| `message_status`          | Outbound message status changed (sent, delivered, failed) |
| `read`                    | A user read your outbound message                         |
| `message_template_status` | A template was approved, rejected, or paused              |
| `account`                 | Account 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)

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  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);
  ```

  ```python Python (Flask) theme={null}
  from flask import Flask, request
  import os, threading

  app = Flask(__name__)
  WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET")

  @app.route("/webhook", methods=["POST"])
  def webhook():
      auth = request.headers.get("Authorization", "")
      if auth != f"Bearer {WEBHOOK_SECRET}":
          return "", 401

      payload = request.get_json()
      threading.Thread(target=process_webhook, args=(payload,), daemon=True).start()
      return "", 200

  def process_webhook(body):
      changes = body.get("entry", [{}])[0].get("changes", [])
      for change in changes:
          value = change.get("value", {})

          for msg in value.get("messages", []):
              contacts = value.get("contacts", [{}])
              print(f"[{msg['type'].upper()}] from {msg['from']} ({contacts[0].get('profile', {}).get('name')})")

          for status in value.get("statuses", []):
              print(f"{status['id']} → {status['status']}")
              if status["status"] == "failed":
                  print("Delivery failed:", status.get("errors"))
  ```

  ```go Go theme={null}
  func webhookHandler(w http.ResponseWriter, r *http.Request) {
      auth := r.Header.Get("Authorization")
      if auth != "Bearer "+os.Getenv("WEBHOOK_SECRET") {
          http.Error(w, "Unauthorized", http.StatusUnauthorized)
          return
      }

      body, _ := io.ReadAll(r.Body)
      w.WriteHeader(http.StatusOK)

      go processWebhook(body) // process asynchronously
  }
  ```
</CodeGroup>

***

## 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`.

<Warning>
  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.
</Warning>

***

## Incoming Message Payloads

### Text Message

```json theme={null}
{
  "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].id`                         | string | WhatsApp Business Account (WABA) ID                                          |
| `value.metadata.phone_number_id`      | string | Meta ID of the phone number that received the message. Critical for routing. |
| `value.metadata.display_phone_number` | string | Human-readable phone number                                                  |
| `value.contacts[0].wa_id`             | string | Sender's WhatsApp ID (phone number)                                          |
| `value.contacts[0].profile.name`      | string | Sender's WhatsApp display name                                               |
| `value.messages[0].from`              | string | Sender's phone number                                                        |
| `value.messages[0].id`                | string | Unique ID of the incoming message                                            |
| `value.messages[0].timestamp`         | string | Unix timestamp (in seconds)                                                  |
| `value.messages[0].type`              | string | `text`, `image`, `audio`, `document`, `interactive`, `location`              |
| `value.messages[0].text.body`         | string | Message content (for `type: "text"`)                                         |

***

### Image Message

```json theme={null}
{
  "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.id`        | Media ID. Use the `/media/{id}` endpoint to download the content. |
| `image.mime_type` | File type (`image/jpeg`, `image/png`)                             |
| `image.sha256`    | Hash for file integrity verification                              |
| `image.caption`   | Caption written by the user (optional)                            |

***

### Audio Message

```json theme={null}
{
  "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.id`        | Media ID for downloading the audio file     |
| `audio.voice`     | `true` = user sent a voice note             |
| `audio.mime_type` | `audio/ogg; codecs=opus`, `audio/mp4`, etc. |

***

### Document Message

```json theme={null}
{
  "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:

```json theme={null}
{
  "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.id`    | The `reply.id` you set when sending the button message |
| `interactive.button_reply.title` | The button label shown to the user                     |

***

### Interactive — List Reply

Received when a user selects an item from a list you sent:

```json theme={null}
{
  "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

```json theme={null}
{
  "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

```json theme={null}
{
  "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

<Note>
  The Gateway automatically deducts the Meta fee when `status: "delivered"` and `pricing.billable: true`.
</Note>

```json theme={null}
{
  "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].id`                       | string | The `wamid` of the message you sent — match with the stored ID             |
| `statuses[0].status`                   | string | `sent`, `delivered`, `read`, `failed`                                      |
| `statuses[0].timestamp`                | string | Unix timestamp of the status change                                        |
| `statuses[0].recipient_id`             | string | Recipient's phone number                                                   |
| `statuses[0].conversation.id`          | string | Meta conversation ID                                                       |
| `statuses[0].conversation.origin.type` | string | Conversation category: `marketing`, `utility`, `authentication`, `service` |
| `statuses[0].pricing.billable`         | bool   | `true` = Meta fee will be deducted, `false` = free tier                    |
| `statuses[0].pricing.category`         | string | Billing category                                                           |

### Read

```json theme={null}
{
  "statuses": [{
    "id": "wamid.HBgNOTA1Mzk5ODc2NTQzFQIAERgSM0Y3RDg5NjREOUNBMkFFNEE3AA==",
    "status": "read",
    "timestamp": "1711900500",
    "recipient_id": "905399876543"
  }]
}
```

### Failed

```json theme={null}
{
  "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].code`    | Meta error code (full list: Meta Docs) |
| `errors[0].title`   | Short error title                      |
| `errors[0].message` | Detailed description                   |

***

## System Event Payloads

### Template Status Update

```json theme={null}
{
  "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` Value      | Description                                      |
| ------------------ | ------------------------------------------------ |
| `APPROVED`         | Template is ready to use                         |
| `REJECTED`         | Template was rejected — check the `reason` field |
| `PENDING_DELETION` | Template is scheduled for deletion               |
| `FLAGGED`          | Template was flagged by Meta                     |
| `PAUSED`           | Paused due to low quality score                  |

### Account Update

```json theme={null}
{
  "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

<Warning>
  **No delivery order guarantee.** `sent` and `delivered` events may not arrive in order. Sort by `statuses[0].timestamp`.
</Warning>

<Note>
  **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).
</Note>

<Tip>
  **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.
</Tip>

***

## Checklist

<Steps>
  <Step title="Is your HTTPS endpoint publicly accessible?">
    Must have a valid SSL certificate and be reachable from the internet.
  </Step>

  <Step title="Are you returning HTTP 200 within 5 seconds?">
    Move DB writes, API calls, and emails to a background job.
  </Step>

  <Step title="Are you validating the Authorization header?">
    Without the `Bearer WEBHOOK_SECRET` check, you're open to forged events.
  </Step>

  <Step title="Is your processing idempotent?">
    Ensure the same `wamid` cannot trigger duplicate side effects.
  </Step>

  <Step title="Have you subscribed to the right events?">
    At minimum, `messages` and `message_status` should be active in the Dashboard.
  </Step>
</Steps>
