> For the complete documentation index, see [llms.txt](https://docs.channel360.co.za/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.channel360.co.za/api-usage/receive-delivery-updates.md).

# Receive delivery updates

Get told when a message is sent, delivered, read or fails, and when a customer writes to you.

Channel360 tells your system what happened to each message — sent, delivered, read, failed — and when a customer writes to you, by calling a URL on your server. This page sets that up.

{% hint style="info" %}
Delivery results, failures, read receipts and customer messages are **only** available through webhooks. If no webhook is registered when they happen, they are not stored for you to collect later.
{% endhint %}

## Before you start

* A public HTTPS URL on your server that accepts `POST` requests with a JSON body.
* An API key, if you register the webhook through the API. See [Authentication](/api-usage/get-started/authentication.md).

## Steps

### 1. Choose your events

An **event** (also called a trigger) is the kind of thing that happened. You choose which events your webhook receives.

| Event                           | When it is sent                                                                                                        | Typical use                                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `notification:delivery:channel` | Your template message was accepted by WhatsApp (sent).                                                                 | Mark "sent".                                                    |
| `notification:delivery:user`    | Your template message reached the customer's phone (delivered).                                                        | Mark "delivered".                                               |
| `notification:delivery:failure` | Your template message failed. The `error` object says why.                                                             | Alert, then fix and resend.                                     |
| `notification:match:failure`    | The message failed before it reached WhatsApp, for example because the number could not be matched to a WhatsApp user. | Check the number.                                               |
| `conversation:read`             | The customer read the conversation.                                                                                    | Mark "read".                                                    |
| `message:appUser`               | The customer sent you a message.                                                                                       | Start or continue a [reply](/api-usage/reply-to-a-customer.md). |
| `message:delivery:channel`      | A reply you sent (text, media, interactive) was accepted by WhatsApp.                                                  | Mark the reply "sent".                                          |
| `message:delivery:user`         | A reply you sent reached the customer's phone.                                                                         | Mark the reply "delivered".                                     |
| `message:delivery:failure`      | A reply you sent failed.                                                                                               | Alert.                                                          |
| `flow:submission`               | The customer submitted a WhatsApp Flow form.                                                                           | Read the answers.                                               |
| `template:status`               | A template was imported, or its approval status changed.                                                               | Refresh your template list.                                     |

The web app shows these with friendly labels, for example **Notification Delivery User**. The API uses the names in the first column.

### 2. Register the webhook

{% tabs %}
{% tab title="In the web app (easiest)" %}

1. Go to **Settings → Developer Settings → Webhooks** and select **Create**.
2. Enter a **Name** and your **Endpoint URL**.
3. Under **Events**, tick the events you want.
4. Select **Create**.

{% content-ref url="/pages/rw2bt83dh9k1qR6ffKiw" %}
[Webhooks](/channel360-guide/webhooks.md)
{% endcontent-ref %}
{% endtab %}

{% tab title="Through the API" %}
`POST` `/org/<ORG_ID>/webhooks`

```bash
curl -X POST "https://channel360.co.za/v1.1/org/<ORG_ID>/webhooks" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order system",
    "target": "https://example.com/channel360/webhook",
    "triggers": [
      "notification:delivery:channel",
      "notification:delivery:user",
      "notification:delivery:failure",
      "conversation:read",
      "message:appUser"
    ]
  }'
```

This call **creates or updates**. If `name` (or `id`) matches a webhook you already have, that webhook is updated and the response is `200`. Otherwise a new webhook is created and the response is `201`:

```json
{
  "message": "Webhook Order system created successfully",
  "data": { "id": "webhook_example_123456", "target": "https://example.com/channel360/webhook", "triggers": ["…"] }
}
```

{% endtab %}
{% endtabs %}

### 3. Reply `200` quickly

Your endpoint must answer with any `2xx` status **within 5 seconds**. Do the real work — database writes, calls to other systems — after replying, or in a background job.

### 4. Ignore duplicates

Every call carries an `X-Channel360-Delivery-Id` header. A retried call carries the **same** value. Store the values you have processed and ignore repeats.

### 5. Route by `trigger`

Every payload has a top-level `trigger` field naming the event.

{% tabs %}
{% tab title="JavaScript (Express)" %}

```javascript
import express from "express";

const app = express();
app.use(express.json());

app.post("/channel360/webhook", (req, res) => {
  res.sendStatus(200); // answer first, then do the work

  const event = req.body;
  switch (event.trigger) {
    case "notification:delivery:user":
      console.log("Delivered:", event.notification._id);
      break;
    case "notification:delivery:failure":
      console.log("Failed:", event.notification._id, event.error);
      break;
    case "message:appUser":
      console.log("Customer wrote:", event.messages[0].text);
      break;
  }
});

app.listen(3000);
```

{% endtab %}

{% tab title="Python (Flask)" %}

```python
from flask import Flask, request

app = Flask(__name__)

@app.post("/channel360/webhook")
def channel360_webhook():
    event = request.get_json()
    trigger = event.get("trigger")

    if trigger == "notification:delivery:user":
        print("Delivered:", event["notification"]["_id"])
    elif trigger == "notification:delivery:failure":
        print("Failed:", event["notification"]["_id"], event.get("error"))
    elif trigger == "message:appUser":
        print("Customer wrote:", event["messages"][0].get("text"))

    # Keep this handler fast; move slow work to a background job.
    return "", 200
```

{% endtab %}
{% endtabs %}

## Check it worked

* Send a test message (see the [Quickstart](/api-usage/get-started/quickstart.md)). Within a few seconds your endpoint receives `notification:delivery:channel`, then `notification:delivery:user`.
* If nothing arrives, open **Settings → Developer Settings → Delivery Logs**. It lists Channel360's attempts to call your URL and the response your server gave.

## Retries

* Channel360 tries each event **up to 5 times over about 15 minutes**. The gaps grow: about 30 seconds, 2 minutes, 5 minutes, then 7½ minutes.
* It **retries** when your server does not answer within 5 seconds, cannot be reached, returns `5xx`, returns `429 Too Many Requests`, or returns a redirect (`3xx`).
* It **stops immediately** when your server returns any other `4xx`, such as `400`, `401` or `404`. Channel360 treats a `4xx` as "never send me this again".
* After the fifth failed attempt, the event is not sent again.

## Example payloads

Use these examples to test your handler and map the fields for each event. They use fictional data.

The payloads include an `appUser._id`: Channel360's ID for one customer on your WhatsApp number. It is created when the customer first interacts with your number, and you need it to [reply](/api-usage/reply-to-a-customer.md). [Identifiers](/api-usage/concepts/identifiers.md) explains every ID you will see.

<details>

<summary><code>notification:delivery:channel</code></summary>

This event is sent when a notification is accepted by the channel (WhatsApp).

```json
{
  "trigger": "notification:delivery:channel",
  "version": "v1.1",
  "app": {
    "_id": "app_example_123456789"
  },
  "timestamp": 1781855992.093,
  "destination": {
    "type": "whatsapp",
    "integrationId": "integration_example_123456",
    "destinationId": "27820000000"
  },
  "isFinalEvent": false,
  "externalMessages": [
    {
      "id": "wamid.EXAMPLE_MESSAGE_ID_1234567890"
    }
  ],
  "notification": {
    "_id": "notification_example_123456"
  },
  "matchResult": {
    "client": {
      "integrationId": "integration_example_123456",
      "externalId": "27820000000",
      "id": "client_example_123456",
      "displayName": "+27 82 000 0000",
      "status": "active",
      "raw": {
        "from": "27820000000",
        "profile": {
          "name": "Example User"
        }
      },
      "lastSeen": "2026-06-18T12:46:11.066Z",
      "linkedAt": "2022-08-11T21:36:21.156Z",
      "_id": "client_record_example_123456",
      "platform": "whatsapp",
      "active": true,
      "blocked": false,
      "primary": true
    },
    "appUser": {
      "_id": "appuser_example_123456",
      "authenticated": false,
      "conversationStarted": true
    },
    "conversation": {
      "_id": "conversation_example_123456"
    }
  }
}
```

</details>

<details>

<summary><code>notification:delivery:user</code></summary>

This event is sent when a notification reaches the user's device.

```json
{
  "trigger": "notification:delivery:user",
  "version": "v1.1",
  "app": {
    "_id": "app_example_123456789"
  },
  "timestamp": 1781855997.352,
  "destination": {
    "type": "whatsapp",
    "integrationId": "integration_example_123456",
    "destinationId": "27820000000"
  },
  "isFinalEvent": true,
  "externalMessages": [
    {
      "id": "wamid.EXAMPLE_MESSAGE_ID_1234567890"
    }
  ],
  "notification": {
    "_id": "notification_example_123456"
  },
  "matchResult": {
    "client": {
      "integrationId": "integration_example_123456",
      "externalId": "27820000000",
      "id": "client_example_123456",
      "displayName": "+27 82 000 0000",
      "status": "active",
      "raw": {
        "from": "27820000000",
        "profile": {
          "name": "Example User"
        }
      },
      "lastSeen": "2026-06-18T12:46:11.066Z",
      "linkedAt": "2022-08-11T21:36:21.156Z",
      "_id": "client_record_example_123456",
      "platform": "whatsapp",
      "active": true,
      "blocked": false,
      "primary": true
    },
    "appUser": {
      "_id": "appuser_example_123456",
      "authenticated": false,
      "conversationStarted": true
    },
    "conversation": {
      "_id": "conversation_example_123456"
    }
  }
}
```

</details>

<details>

<summary><code>conversation:read</code></summary>

This event is sent when the user reads the conversation.

```json
{
  "trigger": "conversation:read",
  "timestamp": 1781856019.856,
  "version": "v1.1",
  "app": {
    "_id": "app_example_123456789"
  },
  "appUser": {
    "_id": "appuser_example_123456",
    "givenName": "Example User",
    "signedUpAt": "2022-08-11T21:36:21.156Z",
    "properties": {},
    "identities": [],
    "authenticated": false,
    "conversationStarted": true
  },
  "conversation": {
    "_id": "conversation_example_123456"
  },
  "client": {
    "integrationId": "integration_example_123456",
    "externalId": "27820000000",
    "id": "client_example_123456",
    "displayName": "+27 82 000 0000",
    "status": "active",
    "raw": {
      "from": "27820000000",
      "profile": {
        "name": "Example User"
      }
    },
    "lastSeen": "2026-06-18T12:46:11.066Z",
    "linkedAt": "2022-08-11T21:36:21.156Z",
    "_id": "client_record_example_123456",
    "platform": "whatsapp",
    "active": true,
    "blocked": false,
    "primary": true
  },
  "source": {
    "type": "whatsapp",
    "integrationId": "integration_example_123456"
  }
}
```

</details>

<details>

<summary><code>notification:delivery:failure</code></summary>

This event is sent when a notification fails to deliver. Use the `error` object to inspect the failure reason returned by the channel.

```json
{
  "trigger": "notification:delivery:failure",
  "version": "v1.1",
  "app": {
    "_id": "app_example_123456789"
  },
  "timestamp": 1781856153.836,
  "destination": {
    "type": "whatsapp",
    "integrationId": "integration_example_123456",
    "destinationId": "27820000000"
  },
  "isFinalEvent": true,
  "notification": {
    "_id": "notification_example_failed_123456"
  },
  "matchResult": {
    "client": {
      "integrationId": "integration_example_123456",
      "externalId": "27820000000",
      "id": "client_example_123456",
      "displayName": "+27 82 000 0000",
      "status": "active",
      "raw": {
        "from": "27820000000",
        "profile": {
          "name": "Example User"
        }
      },
      "lastSeen": "2026-06-18T12:46:11.066Z",
      "linkedAt": "2022-08-11T21:36:21.156Z",
      "_id": "client_record_example_123456",
      "platform": "whatsapp",
      "active": true,
      "blocked": false,
      "primary": true
    },
    "appUser": {
      "_id": "appuser_example_123456",
      "authenticated": false,
      "conversationStarted": true
    },
    "conversation": {
      "_id": "conversation_example_123456"
    }
  },
  "error": {
    "code": "uncategorized_error",
    "underlyingError": {
      "message": "(#132001) Template name does not exist in the translation",
      "code": 132001,
      "type": "OAuthException",
      "error_data": {
        "messaging_product": "whatsapp",
        "details": "template name (example_template_name) does not exist in en"
      },
      "fbtrace_id": "FBTRACE_EXAMPLE_123456"
    }
  }
}
```

</details>

<details>

<summary><code>message:appUser</code></summary>

This event is sent when a client sends an inbound message.

The example uses fictional data. It includes one prior notification.

```json
{
  "trigger": "message:appUser",
  "version": "v1.1",
  "app": {
    "_id": "app_example_123456789"
  },
  "appUser": {
    "_id": "appuser_example_123456",
    "givenName": "Example User",
    "signedUpAt": "2026-07-15T13:19:56.559Z",
    "properties": {},
    "identities": [],
    "authenticated": false,
    "conversationStarted": true
  },
  "conversation": {
    "_id": "conversation_example_123456"
  },
  "client": {
    "_id": "client_record_example_123456",
    "integrationId": "integration_example_123456",
    "externalId": "27820000000",
    "id": "client_example_123456",
    "displayName": "+27 82 000 0000",
    "status": "active",
    "raw": {
      "from": "27820000000",
      "profile": {
        "name": "Example User"
      }
    },
    "lastSeen": "2026-07-15T13:19:56.559Z",
    "linkedAt": "2026-07-15T13:19:56.559Z",
    "platform": "whatsapp",
    "active": true,
    "blocked": false,
    "primary": true
  },
  "recentNotifications": [
    {
      "source": {
        "type": "notification",
        "id": "notification_example_123456"
      },
      "role": "appMaker",
      "authorId": "author_example_123456",
      "type": "text",
      "text": "Would you like us to contact you about this offer?",
      "received": 1784121596.587,
      "actions": [
        {
          "type": "reply",
          "text": "Yes please",
          "uri": "",
          "payload": "Yes please",
          "_id": "action_example_123456"
        }
      ],
      "_id": "notification_message_example_123456"
    }
  ],
  "messages": [
    {
      "role": "appUser",
      "source": {
        "type": "whatsapp",
        "id": "client_example_123456",
        "integrationId": "integration_example_123456",
        "originalMessageId": "wamid.EXAMPLE_MESSAGE_ID_1234567890",
        "originalMessageTimestamp": 1784121593
      },
      "authorId": "appuser_example_123456",
      "name": "Example User",
      "_id": "message_example_123456",
      "type": "text",
      "received": 1784121596.587,
      "text": "Hello"
    }
  ]
}
```

`recentNotifications` is optional. It may be absent when the conversation has no earlier messages.

```json
{
  "trigger": "message:appUser",
  "messages": [
    {
      "type": "text",
      "text": "Hello"
    }
  ]
}
```

</details>

<details>

<summary><code>flow:submission</code></summary>

This event is sent when a user submits a WhatsApp Flow.

The example uses fictional data. Parse `response_json` to access submitted fields.

```json
{
  "events": [
    {
      "id": "flow_submission_example_123456",
      "createdAt": "2026-07-16T08:05:49.907Z",
      "type": "flow:submission",
      "payload": {
        "conversation": {
          "id": "conversation_example_123456",
          "type": "personal"
        },
        "source": {
          "type": "whatsapp",
          "integrationId": "integration_example_123456"
        },
        "user": {
          "id": "appuser_example_123456",
          "authenticated": false
        },
        "passthroughType": "interactive",
        "passthroughPayload": {
          "from": "27820000000",
          "id": "wamid.EXAMPLE_MESSAGE_ID_1234567890",
          "timestamp": "1784189148",
          "type": "interactive",
          "interactive": {
            "type": "nfm_reply",
            "nfm_reply": {
              "response_json": "{\"monthly_income\":\"25000\",\"living_expenses\":\"12000\",\"preferred_store\":\"Example Store\",\"receive_marketing\":true,\"first_name\":\"Example\",\"surname\":\"User\",\"id_number\":\"ID_NUMBER_EXAMPLE\",\"cellphone\":\"27820000000\",\"email\":\"example.user@example.com\",\"flow_token\":\"example-flow-token\"}"
            }
          }
        }
      }
    }
  ],
  "trigger": "flow:submission"
}
```

</details>

## Managing webhooks through the API

* **List:** `GET` `/org/<ORG_ID>/webhooks`. The list is paginated with `page` and `limit`. Add `?id=<WEBHOOK_ID>` or `?name=<NAME>` to get one webhook.
* **Change:** send the `POST` from step 2 again with the same `name`.
* **Delete:** `DELETE` `/org/<ORG_ID>/webhooks/<WEBHOOK_ID>`.

```bash
curl -X DELETE "https://channel360.co.za/v1.1/org/<ORG_ID>/webhooks/<WEBHOOK_ID>" \
  -H "Authorization: Bearer <API_KEY>"
```

| Path parameter | Type   | Description                     |
| -------------- | ------ | ------------------------------- |
| `ORG_ID`       | string | Your organisation ID. Required. |
| `WEBHOOK_ID`   | string | The webhook's `id`. Required.   |

## What can go wrong

| What you see                  | Why                                                                      | What to do                                                       |
| ----------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- |
| Nothing arrives               | Wrong URL, the URL is not public, or the event is not ticked.            | Check **Delivery Logs**, and check the events on the webhook.    |
| The same event arrives twice  | A retry after a slow or failed response.                                 | Ignore repeats using `X-Channel360-Delivery-Id`.                 |
| Events stop after one attempt | Your server returned a `4xx`.                                            | Return `2xx` for everything you receive, even events you ignore. |
| **400** when registering      | `target` is missing, `triggers` is empty, or the `name` is already used. | Fix the body. Use a different `name`, or send the matching `id`. |

## Next

{% content-ref url="/pages/1TK22JZcWTEHl2F9Z1rc" %}
[Reply to a customer](/api-usage/reply-to-a-customer.md)
{% endcontent-ref %}

{% content-ref url="/pages/MMPfEfAxW2dosIuGBfym" %}
[Match replies to your records](/api-usage/match-replies-to-your-records.md)
{% endcontent-ref %}
