---
title: "Incoming messages"
description: "Get started with CM.com's Developer API Documentation for Inbound WhatsApp. Find everything you need to implement Inbound WhatsApp for your business."
canonical: "https://docs.cm.com/messaging/whatsapp/whatsapp-inbound"
---

# Incoming messages

> Get started with CM.com's Developer API Documentation for Inbound WhatsApp. Find everything you need to implement Inbound WhatsApp for your business.

Source: https://docs.cm.com/messaging/whatsapp/whatsapp-inbound

This page describes the inbound (MO) messages and notifications you can receive from WhatsApp users through the CM.com Messaging API. Every inbound WhatsApp event is delivered to your MO webhook as an HTTP request with a JSON body. Use this page to identify each event type, understand its payload and decide how your application should respond.

For the generic behaviour of inbound messages across all channels, see [Incoming messages](https://docs.cm.com/messaging/business-messaging/incoming-messages).

## Before you begin

Make sure that:

- A WhatsApp Business number is connected to your CM.com account.
- An MO webhook is configured for that number, and your endpoint responds with a `2xx` status code.
- The `messageContext` field is enabled on your MO webhook. You need this field to correlate replies with the original message. If your payloads do not contain `messageContext`, contact [CM.com Support](https://www.cm.com/support/) to have it enabled.

:::note[Design for unknown fields]
Meta regularly extends the WhatsApp platform. Parse payloads tolerantly: ignore fields you do not recognise and do not fail on `null` values, so that new attributes do not break your integration.
:::

---

## Payload structure

All inbound WhatsApp events share the same envelope. Channel-specific data is placed in `message.custom`.

| Field | Type | Description |
| :- | :- | :- |
| `reference` | string | Unique identifier of the inbound message. For WhatsApp messages this is the message ID assigned by Meta (`wamid.…`). Store it if you need to correlate replies, deletions or errors. |
| `messageContext` | string | Reference of the message the user replied to. Empty when the message is not a reply. See [Replies](#replies). |
| `from.number` | string | Phone number of the WhatsApp user, or their Business-scoped User ID if the phone number is not shared. |
| `from.name` | string | WhatsApp profile name of the user. |
| `from.whatsapp` | object | The user's `user_id` (Business-scoped User ID), and where applicable `parent_user_id` and `username`. See [Business-scoped User IDs](https://docs.cm.com/messaging/whatsapp/whatsapp-business-scoped-user-ids#receiving-messages). |
| `to.number` | string | Your WhatsApp Business number that received the message. |
| `message.text` | string | Text content of the message. For button and list replies, this contains the title of the selected option. Empty for media messages: a caption is delivered in `message.media.title`. |
| `message.media` | object | Media details (`mediaUri`, `contentType`, `title`). Populated for media messages only. See [Media object](#media-object). |
| `message.custom` | object | WhatsApp-specific data. The contents depend on the event type; see [Event types](#event-types). |
| `message.custom.meta_received_time` | string | Timestamp at which Meta received the message. |
| `message.custom.message_type` | string | Explicit event type, where provided (for example `document`, `location`, `contacts` or `request_welcome`). |
| `message.custom.standby` | boolean | `true` when the message was received while another app held thread control of the conversation (standby mode), `false` otherwise. See [Standby messages](#standby-messages). |
| `message.error` | string | Error description supplied by Meta. Empty unless the message (partially) failed. See [Error notifications](#error-notifications). |
| `groupings` | array | Grouping values associated with the message. |
| `time` | string | Time at which CM.com received the message, in local time (Europe/Amsterdam). |
| `timeUtc` | string | Time at which CM.com received the message, in UTC. |
| `channel` | string | Always `WhatsApp` for the events on this page. |

---

## Event types

Use the table below to determine which kind of event you have received. Check for the indicators in the order listed; the first match identifies the event.

| Event | How to identify it |
| :- | :- |
| [Error notification](#error-notifications) | `message.error` is not empty |
| [Deleted message](#deleted-messages) | `message.custom.deletedMessage` is present |
| [Location message](#location-messages) | `message.custom.message_type` is `location` |
| [Contact message](#contact-messages) | `message.custom.message_type` is `contacts` |
| [Welcome request](#welcome-requests) | `message.custom.message_type` is `request_welcome` |
| [BSUID change](https://docs.cm.com/messaging/whatsapp/whatsapp-business-scoped-user-ids#bsuid-changes) | `message.custom.message_type` is `system` |
| [Thread control update](#thread-control-updates) | `message.custom.message_type` is `thread_control` |
| [Interactive template reply](#interactive-template-replies) | `message.custom.button` is present |
| [Interactive message reply](#interactive-message-replies) | `message.custom.interactive.type` is `list_reply` or `button_reply` |
| [Flows reply](#flows-replies) | `message.custom.interactive.type` is `nfm_reply` |
| [Call permission reply](#call-permission-replies) | `message.custom.interactive.type` is `call_permission_reply` |
| [Product order](#product-orders) | `message.custom.order` is present |
| [Ad referral](#ad-referrals-click-to-whatsapp) | `message.referral` is present |
| [Marketing preference update](#marketing-preference-updates) | See [Marketing preference updates](#marketing-preference-updates) |
| [Media message](#media-messages) | `message.media.mediaUri` is not empty |
| Text message | None of the above; `message.text` contains the message |

Any message can also be a reply to an earlier message. Check `messageContext` independently of the event type. Likewise, check `message.custom.standby` independently of the event type to find out whether the message was received in [standby mode](#standby-messages).

---

## Messages from users

### Media messages

Inbound media messages follow the same support and restrictions as outbound media. See [Media messages](https://docs.cm.com/messaging/business-messaging/incoming-messages#media-messages) for supported formats and size limits.

WhatsApp-specific behaviour:

| WhatsApp content | Received as |
| :- | :- |
| GIF | Video file with content type `video/mp4` |
| Sticker | Media message with content type `image/webp` |

The media file itself is not included in the webhook. Download it from the URL in `message.media.mediaUri`.

#### Media object

| Field | Type | Description |
| :- | :- | :- |
| `mediaUri` | string | URL from which you can download the file. |
| `contentType` | string | MIME type of the file, for example `image/jpeg` or `application/pdf`. |
| `title` | string | The **caption** the user typed with the media. Empty if the user did not add a caption. |

:::warning[The caption is in `media.title`, not in `message.text`]
When a user sends a file with a caption, the caption is delivered in `message.media.title` and `message.text` is empty. `media.title` does **not** contain the original file name. Use `contentType` to determine the file type, and generate your own file name when you store the file.
:::

```json title="Document with caption"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUwxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "",
    "media": {
      "mediaUri": "https://cdn-3.messaging.cm.com/fileproxy/files/39d2aa694f0f474cb1c464e57f26229e",
      "contentType": "application/pdf",
      "title": "Here is the invoice you asked for."
    },
    "custom": {
      "meta_received_time": "2026-09-29T06:08:25",
      "message_type": "document"
    }
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-09-29T06:08:26",
  "channel": "WhatsApp"
}
```

#### Delete media

After you have downloaded and stored a media file, you can delete it from CM.com's servers by sending a `DELETE` request to the exact URL from which you retrieved it (`message.media.mediaUri`).

Media is hosted on the following server:

- `cdn-eu1.messaging.cm.com`

**Request**

```http
DELETE {mediaUri} HTTP/1.1
Host: cdn-eu1.messaging.cm.com
```

**Response**

A successful request returns status code `204` with an empty body. No further action is required.

```http
HTTP/1.1 204 No Content
```

:::warning[Deletion is permanent]
A deleted media file cannot be recovered. Make sure you have stored the file before you delete it.
:::

### Location messages

Users can share a location with your business. The location is provided in `message.custom.location`.

:::note[Live location is not supported]
Only static locations are delivered. Live location updates shared by the user are not forwarded.
:::

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `latitude` | number | Yes | Latitude of the location. Can be negative. |
| `longitude` | number | Yes | Longitude of the location. Can be negative. |
| `label` | string | No | Name or description of the location. |
| `searchQuery` | string | No | Address or search query related to the location. |

```json title="Location message"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUQxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "location": {
        "latitude": 51.603802,
        "longitude": 4.770821,
        "label": "CM HQ",
        "searchQuery": "Konijnenberg 30"
      },
      "meta_received_time": "2024-02-21T11:01:34",
      "message_type": "location"
    }
  },
  "groupings": ["", "", ""],
  "timeUtc": "2024-02-21T11:01:35",
  "channel": "WhatsApp"
}
```

### Contact messages

Users can share one or more contact cards in a single message. The cards are provided as an array in `message.custom.contacts`.

The structure follows WhatsApp's contact format. If you are familiar with the vCard (VCF) format or the address book formats used by Android or iOS, the structure will look familiar.

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `name` | object | Yes | Name of the contact (`formatted_name`, `first_name`, `last_name`, `middle_name`, `prefix`, `suffix`). |
| `phones` | array | No | Phone numbers, each with `phone`, `type` and, if the number is on WhatsApp, `wa_id`. |
| `emails` | array | No | Email addresses of the contact. |
| `addresses` | array | No | Postal addresses (`street`, `city`, `zip`, `country`, `country_code`, `type`). |
| `org` | object | No | Organisation details (`company`, `department`, `title`). |
| `urls` | array | No | Websites of the contact. |
| `birthday` | string | No | Birthday of the contact. |
| `vcard` | string | No | The complete contact card as a Base64-encoded vCard. |
| `origin` | string | No | Origin of the contact card. `contact_request` when the user shared their own number through a [request contact info](https://docs.cm.com/messaging/whatsapp/whatsapp-business-scoped-user-ids#request-phone-number-from-users) button. |

Optional fields that the user did not fill in are returned as `null`.

```json title="Contact message"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUQxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "contacts": [
        {
          "vcard": "QkVHSU46VkNBUkQKVkVSU0lPTjozLjAKTjpjb207Q007OzsKRk46Q00uY29tCk9SRzpDTS5jb20gRGVtbyAyClRJVExFOgpURUw7dHlwZT1Nb2JpbGU7d2FpZD0zMTc2MjAxMTU3MDorMzEgNzYgMjAxIDE1NzAKWC1XQS1CSVotTkFNRTpDTS5jb20KRU5EOlZDQVJE",
          "origin": "other",
          "addresses": [
            {
              "city": "Breda",
              "country": "Netherlands",
              "country_code": "NL",
              "street": "Konijnenberg 30",
              "type": "WORK",
              "zip": "4825 BD"
            }
          ],
          "birthday": null,
          "emails": null,
          "name": {
            "formatted_name": "CM.com",
            "first_name": "CM",
            "last_name": "com",
            "middle_name": null,
            "suffix": null,
            "prefix": null
          },
          "org": {
            "company": "CM.com",
            "department": null,
            "title": null
          },
          "phones": [
            {
              "phone": "+31 76 201 1570",
              "type": "MOBILE",
              "wa_id": "31762011570"
            }
          ],
          "urls": null
        }
      ],
      "meta_received_time": "2026-07-23T09:01:34",
      "message_type": "contacts"
    }
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-07-23T09:01:35",
  "channel": "WhatsApp"
}
```

---

## Replies

On WhatsApp, a user can reply to a specific message. This works both for messages you sent (MT) and for messages the user sent earlier (MO). In both cases, the `messageContext` field of the inbound message tells you which message was replied to.

| User replies to… | `messageContext` contains… |
| :- | :- |
| A message you sent (MT) | The `reference` you set when sending that MT message. |
| One of their own messages (MO) | The `reference` (`wamid.…`) of that earlier MO message. |
| Nothing (not a reply) | An empty string. |

:::warning[Requires the messageContext field]
Without the `messageContext` field on your MO webhook you cannot tell which message was replied to. See [Before you begin](#before-you-begin).
:::

### Replies to your messages (MT)

To recognise replies to a message you sent, set a `reference` on the outbound message.

```json title="Send request"
{
  "messages": {
    "authentication": {
      "productToken": "YOUR_PRODUCT_TOKEN"
    },
    "msg": [
      {
        "from": "CM.com",
        "to": [
          { "number": "0031612345678" }
        ],
        "allowedChannels": ["WhatsApp"],
        "reference": "webinar-invite-2026-10",
        "body": {
          "type": "auto",
          "content": "Reply to this message if you want to receive an invitation to our next webinar."
        }
      }
    ]
  }
}
```

When the user replies to that message, `messageContext` contains the reference you set:

```json title="Inbound reply"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUIxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "webinar-invite-2026-10",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "I would like an invitation to the webinar.",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {}
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-10-05T08:32:33",
  "channel": "WhatsApp"
}
```

:::note[Use unique references]
If you reuse the same reference for multiple messages, you cannot tell which of them the user replied to. Use a unique reference per message where you need reply tracking.
:::

### Replies to the user's own messages (MO)

To recognise replies to earlier inbound messages, store the `reference` of every inbound message you receive. For WhatsApp, this is the message ID assigned by Meta (`wamid.…`).

The user first sends a message:

```json title="Original message"
{
  "reference": "wamid.HBgLMTY0NjcwNDM1OTUVAgARGBI1RjQyNUE3NEYxMzAzMzQ5MkEA",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "I would like to reserve a table for four, please.",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {}
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-10-05T08:32:33",
  "channel": "WhatsApp"
}
```

The user then replies to that message. The new message has its own `reference`, and `messageContext` contains the `reference` of the original message:

```json title="Reply to original message"
{
  "reference": "wamid.HBgLMzQ2ODUxMjA0NzMVAgASGBYzRUIwOEU5NzhENEIzNkI5MDE2QkE3AA",
  "messageContext": "wamid.HBgLMTY0NjcwNDM1OTUVAgARGBI1RjQyNUE3NEYxMzAzMzQ5MkEA",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Scratch that, a friend can't make it. Could you make it a table for three, please?",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {}
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-10-05T08:35:12",
  "channel": "WhatsApp"
}
```

### Replying to a user's message

You can also send a message as a reply to a specific message from the user. The user then sees your message quoted against their original message in WhatsApp.

Take the `reference` of the inbound message:

```json title="Inbound message"
{
  "reference": "wamid.HBgLMzQ2ODUxMjA0NzMVAgASGBYzRUIwOEU5NzhENEIzNkI5MDE2QkE3AA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "I would like an invitation to the webinar.",
    "custom": {},
    "error": ""
  },
  "groupings": [],
  "time": "2024-09-17 11:37:31",
  "timeUtc": "2024-09-17T09:37:31",
  "channel": "WhatsApp"
}
```

Then pass it as `context.message_id` in the `richContent.conversation` of your outbound message:

```json title="Send request"
{
  "messages": {
    "authentication": {
      "productToken": "YOUR_PRODUCT_TOKEN"
    },
    "msg": [
      {
        "from": "CM.com",
        "to": [
          { "number": "0031612345678" }
        ],
        "allowedChannels": ["WhatsApp"],
        "reference": "webinar-invite-confirmation",
        "body": {
          "type": "auto",
          "content": "Fallback text"
        },
        "richContent": {
          "conversation": [
            {
              "text": "Great! We will send you the invitation soon. Thank you for your interest in our webinar.",
              "context": {
                "message_id": "wamid.HBgLMzQ2ODUxMjA0NzMVAgASGBYzRUIwOEU5NzhENEIzNkI5MDE2QkE3AA=="
              }
            }
          ]
        }
      }
    ]
  }
}
```

---

## Interactive responses

### Interactive template replies

When a user taps a button in an interactive template, the inbound message contains a `message.custom.button` object that identifies the button.

| Field | Type | Description |
| :- | :- | :- |
| `label` | string | Text of the button the user tapped. |
| `payload` | string | The payload you assigned to this button when sending the template. Assign a unique payload per button to identify it reliably. |

For `quick_reply` buttons, the button text is also included in `message.text`.

```json title="Template button reply"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUMxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Yes, this is OK",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "button": {
        "label": "Yes, this is OK",
        "payload": "aGlzIHRoaXMgaXMgY29vZHNhc2phZHdpcXdlMGZoIGFTIEZISUQgV1FEV0RT"
      },
      "meta_received_time": "2024-02-21T11:01:34"
    }
  },
  "groupings": ["", "", ""],
  "timeUtc": "2024-02-21T11:01:35",
  "channel": "WhatsApp"
}
```

### Interactive message replies

Interactive messages are sent within the customer service window and are different from interactive _templates_. For how to send them, see [Interactive messages](https://docs.cm.com/messaging/whatsapp/whatsapp-interactive-messages).

The reply is provided in `message.custom.interactive`. The `type` field tells you which kind of interaction took place, and the title of the selected row or button is also included in `message.text`.

| `type` | Triggered when… | Object | Fields |
| :- | :- | :- | :- |
| `list_reply` | The user selects a row in a list | `list_reply` | `id`, `title`, `description` |
| `button_reply` | The user taps a reply button | `button_reply` | `id`, `title` |

The `id` is the postback ID you assigned to the row or button when sending the message. Use it, rather than the title, to identify the user's choice.

```json title="List reply"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUQxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "list-row-title",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "interactive": {
        "type": "list_reply",
        "list_reply": {
          "id": "your-unique-postback-id",
          "title": "list-row-title",
          "description": "list-row-description"
        },
        "button_reply": null
      },
      "meta_received_time": "2024-02-21T11:01:34"
    }
  },
  "groupings": ["", "", ""],
  "timeUtc": "2024-02-21T11:01:35",
  "channel": "WhatsApp"
}
```

```json title="Button reply"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUQxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "button-title",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "interactive": {
        "type": "button_reply",
        "list_reply": null,
        "button_reply": {
          "id": "your-unique-postback-id",
          "title": "button-title"
        }
      },
      "meta_received_time": "2024-02-21T11:01:34"
    }
  },
  "groupings": ["", "", ""],
  "timeUtc": "2024-02-21T11:01:35",
  "channel": "WhatsApp"
}
```

### Flows replies

When a user completes a WhatsApp Flow, you receive an interactive message of type `nfm_reply`. The submitted data is in `message.custom.interactive.nfm_reply.response_json`.

| Field | Description |
| :- | :- |
| `nfm_reply.name` | Always `flow`. |
| `response_json.flow_token` | The `flow_token` you sent with the original Flows message. Use it to correlate the response. |
| `response_json.*` | Every other property contains data entered by the user. The property names are those defined in your Flow, so they differ per Flow. |

```json title="Flows reply"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUUxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "interactive": {
        "type": "nfm_reply",
        "list_reply": null,
        "button_reply": null,
        "nfm_reply": {
          "name": "flow",
          "response_json": {
            "flow_token": "your-flow-token",
            "entered-field-1": "entered-value-1",
            "entered-field-2": "entered-value-2"
          }
        }
      },
      "meta_received_time": "2024-02-21T11:01:34"
    },
    "error": ""
  },
  "groupings": ["", "", ""],
  "timeUtc": "2024-02-21T11:01:35",
  "channel": "WhatsApp"
}
```

### Call permission replies

You receive a call permission reply when a user grants, refuses or loses permission for your business to call them. This happens when:

- The user accepts or rejects a call permission request you sent.
- The user calls your business, which grants permission automatically.
- Four consecutive business-initiated calls go unanswered, which revokes permission automatically.

The details are in `message.custom.interactive.call_permission_reply`.

| Field | Type | Description |
| :- | :- | :- |
| `response` | string | `accept` or `reject`. |
| `is_permanent` | boolean | `true` if the permission does not expire. |
| `expiration_timestamp` | string \| null | For temporary permissions, the moment the permission expires. `null` otherwise. |
| `response_source` | string | What triggered the update, for example `user_action`. |

:::note[Expiry is not notified]
No webhook is sent when a temporary permission expires. Use `expiration_timestamp` to track expiry in your own system.
:::

```json title="Accepted (permanent)"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUYxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Received call permission reply.",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "interactive": {
        "type": "call_permission_reply",
        "list_reply": null,
        "button_reply": null,
        "nfm_reply": null,
        "payment_method": null,
        "call_permission_reply": {
          "response": "accept",
          "expiration_timestamp": null,
          "is_permanent": true,
          "response_source": "user_action"
        }
      },
      "meta_received_time": "2026-08-04T12:58:43"
    },
    "error": ""
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-08-04T12:58:44",
  "channel": "WhatsApp"
}
```

```json title="Accepted (temporary)"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUYxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Received call permission reply.",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "interactive": {
        "type": "call_permission_reply",
        "list_reply": null,
        "button_reply": null,
        "nfm_reply": null,
        "payment_method": null,
        "call_permission_reply": {
          "response": "accept",
          "expiration_timestamp": "2026-08-05T12:58:43",
          "is_permanent": false,
          "response_source": "user_action"
        }
      },
      "meta_received_time": "2026-08-04T12:58:43"
    },
    "error": ""
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-08-04T12:58:44",
  "channel": "WhatsApp"
}
```

```json title="Rejected"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUYxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Received call permission reply.",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "interactive": {
        "type": "call_permission_reply",
        "list_reply": null,
        "button_reply": null,
        "nfm_reply": null,
        "payment_method": null,
        "call_permission_reply": {
          "response": "reject",
          "expiration_timestamp": null,
          "is_permanent": false,
          "response_source": "user_action"
        }
      },
      "meta_received_time": "2026-08-04T12:58:43"
    },
    "error": ""
  },
  "groupings": ["", "", ""],
  "timeUtc": "2026-08-04T12:58:44",
  "channel": "WhatsApp"
}
```

### Product orders

When a user sends you their shopping cart from a product message, you receive an order. You can use it to confirm the order and, for example, send the user a payment link. For how to send product messages, see [Product messages](https://docs.cm.com/messaging/whatsapp/whatsapp-catalogue-product-messages).

The order is provided in `message.custom.order`.

| Field | Type | Description |
| :- | :- | :- |
| `catalog_id` | string | ID of the catalogue the products belong to. |
| `product_items` | array | The products in the cart. |
| `product_items[].product_retailer_id` | string | Your retailer ID of the product. |
| `product_items[].quantity` | string | Quantity ordered. |
| `product_items[].item_price` | string | Unit price of the product. |
| `product_items[].currency` | string | ISO 4217 currency code of the price. |
| `text` | string | Text the user added to the order, or `null`. |

:::warning[Numeric values are strings]
`quantity` and `item_price` are delivered as strings. Convert them before performing calculations, and always validate prices against your own catalogue.
:::

```json title="Product order"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUcxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "order": {
        "catalog_id": "123456789012345",
        "product_items": [
          {
            "product_retailer_id": "cmcomdevelop-frisbee",
            "quantity": "2",
            "item_price": "15",
            "currency": "EUR"
          },
          {
            "product_retailer_id": "cmcomdevelop-coaster",
            "quantity": "6",
            "item_price": "7",
            "currency": "EUR"
          },
          {
            "product_retailer_id": "cmcomdevelop-mousepad",
            "quantity": "1",
            "item_price": "9.99",
            "currency": "EUR"
          }
        ],
        "text": null
      },
      "meta_received_time": "2021-11-08T12:19:48"
    }
  },
  "groupings": ["", "", ""],
  "time": "2021-11-08 13:19:49",
  "timeUtc": "2021-11-08T12:19:49",
  "channel": "WhatsApp"
}
```

---

## Notifications

### Deleted messages

When a user deletes a message they sent you, you receive a notification. `message.custom.deletedMessage` contains the `reference` of the deleted message, as received in an earlier webhook.

:::danger[️ Remove deleted content]
When you receive this notification, remove the referenced message from your systems.
:::

```json title="Deleted message"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUgxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "deletedMessage": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUQxMjM0NTY3ODkwQUJDREVGAA==",
      "meta_received_time": "2021-11-08T12:19:48"
    }
  },
  "groupings": ["", "", ""],
  "time": "2021-11-08 13:19:49",
  "timeUtc": "2021-11-08T12:19:49",
  "channel": "WhatsApp"
}
```

### Error notifications

When Meta cannot (fully) deliver a message from a user, you still receive a notification so you know the user tried to contact you. The reason is provided by Meta in `message.error`, for example `Message type is not currently supported`.

Consider responding to the user, for example by asking them to send their message in a different format.

```json title="Error notification"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUkxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "meta_received_time": "2022-11-08T12:19:48"
    },
    "error": "Message type is not currently supported"
  },
  "groupings": ["", "", ""],
  "time": "2022-11-08 13:19:49",
  "timeUtc": "2022-11-08T12:19:49",
  "channel": "WhatsApp"
}
```

### Welcome requests

When this feature is activated, you receive a notification the first time a user opens a chat with your business, before they have sent a message. Use it to greet the user with a personalised welcome message, which is particularly useful for customer service and account management.

The notification is sent only if no message thread exists yet between the user and your business number. It is identified by `message.custom.message_type` with the value `request_welcome`.

```json title="Welcome request"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUoxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "User request welcome message",
    "custom": {
      "meta_received_time": "2022-11-08T12:19:48",
      "message_type": "request_welcome"
    }
  },
  "groupings": ["", "", ""],
  "time": "2022-11-08 13:19:49",
  "timeUtc": "2022-11-08T12:19:49",
  "channel": "WhatsApp"
}
```

### Standby messages

Only one app at a time holds thread control of a WhatsApp conversation. While another app, for example the Meta Business Agent, holds thread control, your application is in standby mode for that conversation. Messages that users send in the meantime are still delivered to your MO webhook, in the same format as any other message. They are marked with `message.custom.standby` set to `true`.

Messages received in the normal way carry `message.custom.standby` set to `false`.

Use the flag to decide how to handle the message, for example to store it without replying while another app is handling the conversation.

```json title="Text message received in standby mode"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUsxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Where is my order?",
    "media": {
      "mediaUri": "",
      "contentType": "",
      "title": ""
    },
    "custom": {
      "meta_received_time": "2026-10-02T12:38:54",
      "message_type": "text",
      "standby": true
    }
  },
  "groupings": ["", "", ""],
  "time": "2026-10-02 12:38:55",
  "timeUtc": "2026-10-02T10:38:55",
  "channel": "WhatsApp"
}
```

:::note[What is not forwarded]
Delivery and read receipts for messages that other apps sent during standby are not forwarded to your webhooks. Copies (echoes) of those messages are not forwarded yet either, but will be in the near future.
:::

### Thread control updates

When thread control of a conversation changes hands, you receive a notification as an incoming message. It is identified by `message.custom.message_type` with the value `thread_control`.

Unlike other incoming messages, a thread control update is not a message from the user:

- `reference` contains the identifier of the Meta webhook entry, not a message ID (`wamid.…`).
- `from.number` is the phone number of the user whose conversation changed hands.
- `message.text` contains a short summary of the change, for example `Thread control passed to escalation.`
- `message.custom` describes the change, as listed below.

| Field | Type | Description |
| :- | :- | :- |
| `message.custom.message_type` | string | Always `thread_control`. |
| `message.custom.type` | string | The kind of change: `control_taken` or `control_passed`. |
| `message.custom.consumer_phone_number` | string | Phone number of the user whose conversation changed hands. |
| `message.custom.timestamp` | string | Unix timestamp, in seconds, of when thread control changed. |
| `message.custom.control_taken` | object | Present when `type` is `control_taken`: another app took thread control away from your application. See [Control taken](#control-taken). |
| `message.custom.control_passed` | object | Present when `type` is `control_passed`: thread control was passed on, for example from an AI agent to a human. See [Control passed](#control-passed). |

#### Control taken

| Field | Type | Description |
| :- | :- | :- |
| `previous_owner_app_id` | string | ID of the app that held thread control before. |
| `new_owner_app_id` | string | ID of the app that now holds thread control. |
| `new_owner_business_id` | string | ID of the business that owns the new app. |
| `metadata` | string | Free-text information supplied with the change. |

```json title="Control taken"
{
  "reference": "102290129340398",
  "messageContext": "",
  "from": {
    "number": "0031612345678"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Thread control taken by app 1143680903703001.",
    "custom": {
      "message_type": "thread_control",
      "type": "control_taken",
      "consumer_phone_number": "31612345678",
      "timestamp": "1790937534",
      "control_taken": {
        "previous_owner_app_id": "1143680903703002",
        "new_owner_app_id": "1143680903703001",
        "new_owner_business_id": "102290129340398",
        "metadata": "Human agent stepping in"
      }
    }
  },
  "time": "2026-10-02 12:38:55",
  "timeUtc": "2026-10-02T10:38:55",
  "channel": "WhatsApp"
}
```

#### Control passed

| Field | Type | Description |
| :- | :- | :- |
| `previous_owner_app_id` | string | ID of the app that held thread control before. |
| `previous_owner_app_role` | string | Role of that app, for example `meta_business_agent`. |
| `previous_owner_role` | string | Role of the previous owner in the conversation, for example `ai_agent`. |
| `new_owner_role` | string | Role that now holds thread control, for example `escalation`. |
| `metadata` | string | Free-text information supplied with the change, for example the reason for the escalation. |

```json title="Control passed"
{
  "reference": "102290129340398",
  "messageContext": "",
  "from": {
    "number": "0031612345678"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Thread control passed to escalation.",
    "custom": {
      "message_type": "thread_control",
      "type": "control_passed",
      "consumer_phone_number": "31612345678",
      "timestamp": "1790937534",
      "control_passed": {
        "previous_owner_app_id": "1143680903703001",
        "previous_owner_app_role": "meta_business_agent",
        "previous_owner_role": "ai_agent",
        "new_owner_role": "escalation",
        "metadata": "business_missing_info"
      }
    }
  },
  "time": "2026-10-02 12:38:55",
  "timeUtc": "2026-10-02T10:38:55",
  "channel": "WhatsApp"
}
```

### Marketing preference updates

You receive a notification when a user changes their preference for receiving marketing messages from your business:

- The user stops marketing messages (opt-out).
- The user resumes marketing messages (opt-in).

The change is described in `message.text`.

:::warning[Respect opt-outs]
When a user opts out, update your contact records and stop sending them marketing templates until they opt back in.
:::

```json title="Resumed marketing messages"
{
  "reference": "48b65fde-f73a-4c77-b600-a581807931ed",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "User requested to resume marketing messages",
    "custom": {
      "meta_received_time": "2025-12-05T11:00:34"
    },
    "error": ""
  },
  "time": "2025-12-05 12:00:35",
  "timeUtc": "2025-12-05T11:00:35",
  "channel": "WhatsApp"
}
```

---

## Ad referrals (Click-to-WhatsApp)

When a user starts a conversation by clicking an ad or post with a Click-to-WhatsApp, Click-to-Facebook or Click-to-Instagram call to action, the first inbound message contains a `message.referral` object. It tells you which ad or post the user came from.

Use the click ID (`ctwa_clid`) to send conversion events back to Meta through the Meta Conversions API. Meta Ads Manager uses these events to measure and optimise your campaigns, for example to retarget users who did not convert.

:::note[Availability]
The Meta Conversions API for WhatsApp is only available to businesses based outside the United Kingdom, the European Union and Japan.
:::

| Field | Type | Description |
| :- | :- | :- |
| `source_url` | string | Meta URL of the ad or post. |
| `source_id` | string | Meta ID of the ad or post. |
| `source_type` | string | `ad` or `post`. |
| `headline` | string | Headline of the ad or post. |
| `body` | string | Body text of the ad or post. |
| `media_type` | string | Type of media in the ad or post, if any: `image` or `video`. |
| `image_url` | string | URL of the image, if the ad or post contains one. |
| `ctwa_clid` | string | Click ID assigned by Meta. Required when sending conversion events to Meta. |

```json title="Ad referral"
{
  "reference": "wamid.HBgLMzE2MTIzNDU2NzgVAgASGBQzQUsxMjM0NTY3ODkwQUJDREVGAA==",
  "messageContext": "",
  "from": {
    "number": "0031612345678",
    "name": "John Doe"
  },
  "to": {
    "number": "0031850123456"
  },
  "message": {
    "text": "Hi, I'd like to know more about your offer.",
    "custom": {
      "meta_received_time": "2025-06-30T10:23:34"
    },
    "referral": {
      "source_url": "https://fb.me/xxxxxxxxx",
      "source_id": "120200000000000000",
      "source_type": "ad",
      "headline": "Summer sale",
      "body": "Chat with us on WhatsApp for 20% off.",
      "media_type": "image",
      "image_url": "https://scontent.xx.fbcdn.net/xxxxxxxx.jpg",
      "ctwa_clid": "AfezXXXXlzjkdgEtWs6Coke3jFaNpq7eUgd4H76BmKbOZFw31hIHf-7JEPkRjs_kUVIF0CMzfBkVux_XXX2ZzVPPja_GuHXXXXi9Nt0l3r5d4BZPwNQJpAiQNJ2E32nOcVDA"
    }
  },
  "groupings": ["", "", ""],
  "time": "2025-06-30 12:23:34",
  "timeUtc": "2025-06-30T10:23:34",
  "channel": "WhatsApp"
}
```
