---
title: "WhatsApp Thread Control"
description: "Developer guide: what your integration receives, sends and has to set up"
canonical: "https://docs.cm.com/messaging/whatsapp/whatsapp-thread-control-v3"
---

# WhatsApp Thread Control

> Developer guide: what your integration receives, sends and has to set up

Source: https://docs.cm.com/messaging/whatsapp/whatsapp-thread-control-v3

_Developer guide: what your integration receives, sends and has to set up_

When several responders share one WhatsApp number, Meta assigns every conversation to exactly one of them. Meta reports these changes to CM, and CM forwards them to your integration on your **existing MO webhook**. You move a conversation yourself by sending a **thread control MT Event** to CM.

| Term | Meaning in this guide |
| - | - |
| MO | A message CM delivers to you on your MO webhook (inbound). |
| MT | A message you send to the WhatsApp user through the CM Business Messaging (BM) API. |
| SR | The status report of an MT you sent. |
| MT Event | An action you send to CM that is not a message, such as mark as read, typing, or the thread control actions take, pass and release (section 8). |
| Service message | A free-form (non-template) message, typically a reply to the user. |
| Template | A pre-approved marketing, utility or authentication message. |

> **Reading guide**
>
> **\[New]** marks something that is new on your existing MO and SR webhook. Phone numbers, IDs and timestamps in the examples are illustrative.

## 1. The goal of thread control

A WhatsApp conversation is a **thread** between the business number and one WhatsApp user. Thread control decides **who is allowed to answer in that thread**, so that the user does not get two answers to one question, and two systems do not talk over each other.

- **One lead per thread.** At any moment one responder leads the thread (Meta calls it the _thread owner_), or nobody does (_idle_). Only the lead may send Service messages. The one exception is the escalation partner: its Service message takes the thread (section 3.1).
- **Everyone else stays informed.** Responders that do not lead are in _standby_. When the business allows it, they receive a copy of the user's messages as context. They can still send templates.
- **Control can move.** The lead can hand a thread to another role (_pass_) or give it up (_release_). The escalation partner can _take_ any thread. Typical case: an AI agent answers first and hands over to a human agent.

Thread control is not a feature you switch on in your integration. It is the result of how the business configures Conversation Routing in Meta Business Suite. Your integration has to recognise the new signals and respect them.

## 2. Who and when

### 2.1 Who needs this

| You are... | Why thread control matters to you |
| - | - |
| A customer integrating on the CM BM API | You share the WhatsApp number with another responder, such as the Meta Business Agent (MBA) or a partner platform. You receive new MO types, and your Service messages can be rejected. |
| A partner platform on the same number | You hold one role (for example `marketing`). Other responders may take the conversation from you or hand it to you. |
| A developer building the handler | You need to recognise the signals, track the thread state, and avoid sending Service messages when you do not lead. Sections 6 to 8 and 10 are for you. |

### 2.2 When does it apply

Thread control only occurs when **Conversation Routing** is active on the WhatsApp account. That is the case when the account has more than one responder and the business has configured routing in Meta Business Suite. Without routing, every responder receives every message and none of the signals in this guide occur. Two typical situations:

- An AI agent (Meta Business Agent) and your human-agent system share one number (section 4).
- Two or more platforms share one number, each with its own role, for example `marketing` and `utility` (section 5).

## 3. Roles

### 3.1 Two kinds of role

Two things are called a role, so keep them apart. A **role identifier** says what a responder is on the account (for example `marketing`). A **position** says what a responder is in one conversation: the lead or standby. The escalation partner is a special responder that can become the lead of any conversation.

| Position | Meaning |
| - | - |
| Lead | Owns the thread. Receives the user's messages and may send Service messages. |
| Standby | Does not own the thread. Receives copies of the user's messages only when the business granted Conversation visibility. Must not send Service messages and cannot pass or release. |
| Escalation partner | The one responder (at most one per account) that can take any thread, and that receives threads passed to the `escalation` role. When it is not leading, it is in standby like any other responder. |

### 3.2 Role identifiers

The roles can be configured in the Conversation Routing domain in the Meta Business Suite under the WhatsApp Account > more > conversation routing.

| Identifier | Who it is | Notes |
| - | - | - |
| `customer_service` | Primary for the Service entry point (a message that is not a reply to a recent template or ad). | Configured by the business in Meta Business Suite. |
| `marketing` | Primary for replies to marketing templates. | Configured by the business in Meta Business Suite. |
| `utility` | Primary for replies to utility templates. | Configured by the business in Meta Business Suite. |
| `ctwa` | Primary for Click-to-WhatsApp ad conversations. | Configured by the business in Meta Business Suite. |
| `ai_agent` | The Meta Business Agent (MBA). | The only role the business does not configure. Only available on accounts where MBA is enabled. |
| `escalation` | The escalation partner. | At most one per account. The only role that can take a thread. Configured by the business in Meta Business Suite. |
| `calls` | Primary for WhatsApp calls. | Never appears in thread control events: handling a call never makes a responder the lead. Configured by the business in Meta Business Suite. |

The same identifiers appear in thread control MOs (`previous_owner_role`, `new_owner_role`) exactly as Meta sends them, and they are the values you use as `target_role` when you pass a thread (section 8.3). A responder can hold more than one role.

## 4. Thread control with the Meta Business Agent

Situation: the Meta Business Agent (MBA, role `ai_agent`) answers first. When the user needs a human, MBA hands the conversation to the **escalation partner**. For this flow the business configures **your CM integration as the escalation partner** in Meta Business Suite. You are in standby until MBA hands over or you take the thread, then you lead and answer through the BM API.

| # | What happens | What you receive | What you do |
| - | - | - | - |
| 1 | The user writes. MBA leads the thread. | MO with `standby: true`, only if the business granted Conversation visibility. | Store as context. Do not reply. |
| 2 | MBA answers the user. | Nothing. CM does not forward messages that other responders send. | No action. |
| 3a | MBA hands the thread over to you. | MO `thread_control` with `type: control_passed`. It can include a `conversation_context` summary. | You are the lead. Start your own handling, for example assign a human agent, and reply with a Service message. |
| 3b | Or: you step in yourself while MBA is answering. | Nothing for your own take. Your next MOs have `standby: false`. MBA gets `control_taken`. | Send a `ThreadControlTake` MT Event (section 8.2), or simply send a Service message: as escalation partner it takes the thread. |
| 4 | You answer the user. | SR of your MT. | Send Service messages through the BM API. |
| 5 | You are done with the conversation. | Nothing. No event follows a release. | Send a `ThreadControlRelease` MT Event. The thread becomes idle and the user's next message is routed again, for example to MBA. Or send a `ThreadControlPass` with `target_role: ai_agent` to hand it back to MBA right away. |

### 4.1 Actions you can take

| Action (MT Event) | When | Result |
| - | - | - |
| `ThreadControlTake` | You want the thread before MBA hands it over. Escalation partner only. | You lead. MBA gets `control_taken`. Sending a Service message has the same effect (implicit take). |
| `ThreadControlPass` | You lead and want to hand the thread to another role, for example back to `ai_agent`. | The target role leads and gets `control_passed`. |
| `ThreadControlRelease` | You lead and the conversation is finished. | The thread becomes idle. Nobody is notified. |

- **Every Service message you send as escalation partner takes the thread from MBA.** Only send one when you intend to take over the conversation. Templates never take the thread.
- **Release or pass the thread when you are done.** Otherwise you stay the lead, and MBA does not answer, until the user has been inactive for 24 hours, or until the user clicks a Click-to-WhatsApp ad or taps a button on a marketing or utility template (that routes the thread again).
- Use `control_passed` as the trigger to start your own handling of the conversation.
- Your role is configured by the business in Meta Business Suite. If your integration is not the escalation partner, MBA's handover does not reach you and `ThreadControlTake` is rejected by Meta.

## 5. Thread control with several partners

Situation: more than one platform shares the WhatsApp number. Example: a partner platform sends marketing templates and is primary for the `marketing` role. You, the customer through CM, send utility templates and are primary for the `utility` role. Meta routes every inbound message to one of you.

### 5.1 Who holds which role

| Who | Role | Sends |
| - | - | - |
| Partner platform | `marketing` | Marketing templates. Handles replies to marketing templates. |
| You (customer, via the CM BM API) | `utility` | Utility templates. Handles replies to utility templates. |
| Meta Business Agent (optional) | `ai_agent` | Service messages while it leads. |
| Escalation partner (optional) | `escalation` | Any message. Can take any thread. |

### 5.2 Example flow

| # | What happens | What each side sees |
| - | - | - |
| 1 | Both send templates to the user. | Templates need no lead and do not change the lead. Nobody leads yet: the thread is idle. |
| 2 | The user replies to the marketing template. | Meta routes it to the partner platform, which becomes the lead and gets a regular message. You are in standby and get an MO with `standby: true` (only with Conversation visibility). |
| 3 | You try to send a Service message. | Rejected with a failed SR (error 131070). Send a template or wait. |
| 4 | The user taps a quick-reply button on your utility template. | Meta routes the thread again and you become the lead. You get a regular MO with `standby: false`. There is no `control_passed` for this: it is a re-route, not a handover. The partner platform is now in standby. |
| 5 | You answer with a Service message. | Allowed: you lead. |
| 6 | Optional: you pass the thread back. | Send `ThreadControlPass` with `target_role: marketing`. The partner platform gets `control_passed` and leads again. |

### 5.3 Things to know

- **Use the `standby` field to know your position.** Read `message.custom.standby` on every regular MO: `true` means another responder leads, `false` means you lead. This also covers changes that arrive without a thread control MO, such as a release, 24 hours of user inactivity, or a template button tap that routes the thread to another responder.
- **Templates are always safe to send.** Marketing, utility and authentication templates are allowed in every position and sending one never changes the lead.
- **The user's reaction can change the lead.** A button tap, list selection or Flow reply on a marketing or utility template, or a Click-to-WhatsApp ad click, routes the thread again. Authentication templates never change the lead.

## 6. Thread control signals

Thread control reaches you through your existing MO and SR webhook (you receive), and you act on it through MT Events and the BM API (you send). There is nothing new to subscribe to: CM receives Meta's routing and handover webhooks and forwards what is relevant to you.

### 6.1 What you receive

| Signal | When you receive it | What to do |
| - | - | - |
| **\[New]** MO with `standby: false` | A user message on a thread you lead. | Answer it with a Service message. |
| **\[New]** MO with `standby: true` | A copy of a user message on a thread another responder leads. Only when the business granted you Conversation visibility. | Context only. Do not reply. |
| **\[New]** `conversation_context` on an MO | An AI-generated summary, when you pick up a conversation without having had standby copies of it (section 7.3). | Show it to your agent. It is optional: never rely on it being present. |
| **\[New]** MO `control_passed` | Another responder passed the thread to a role you hold. Only the new lead receives it. | You are the lead now. You may send Service messages. |
| **\[New]** MO `control_taken` | The escalation partner took a thread you led, by an explicit take or by sending a Service message. Only the previous lead receives it. | You no longer lead. Stop sending Service messages. |
| **\[New]** SR with error 131070 | Your Service message was rejected because you do not lead the thread, or the thread is idle. | Do not retry. Mark the conversation as not led by you. |

> **What you do not receive**
>
> - **Copies of messages other responders send** (Meta calls them echoes) and **their delivery or read receipts**. CM does not forward these.
> - **Any event for a release, for 24 hours of user inactivity, for a re-route** (button tap, ad click), or for your own take or pass. Meta sends no event for these. Track them yourself (section 10, step 5).
> - **A status report for a thread control MT Event**, neither on success nor on rejection (section 8.5).

### 6.2 What you can send, by position

| Action | You lead | You are in standby | You are the escalation partner (not leading) | Notes |
| - | - | - | - | - |
| Service message (BM API) | Yes | No (SR 131070) | Yes | As escalation partner it takes the thread (implicit take). |
| Template: marketing, utility, authentication (BM API) | Yes | Yes | Yes | Never changes the lead. |
| `ThreadControlPass` (MT Event) | Yes | No | No | Lead only. As escalation partner, always set `target_role`: Meta rejects an untargeted pass from you (error 100). |
| `ThreadControlRelease` (MT Event) | Yes | No | No | Lead only. The escalation partner must take the thread first. |
| `ThreadControlTake` (MT Event) | Not needed | No (Meta error 2494191) | Yes | Escalation partner only. |

### 6.3 How to recognise the signals

All signals are recognised from the `custom` object of an MO, or from the description of an SR. Never parse `message.text` of a thread control MO.

| Signal | How to recognise it | What it means |
| - | - | - |
| **\[New]** MO `standby: false` | `message.custom.standby` is `false` | You lead. Handle as a customer message. |
| **\[New]** MO `standby: true` | `message.custom.standby` is `true` | Another responder leads. Context only. |
| **\[New]** Conversation summary | `message.custom.conversation_context` is present | Human-readable recap. Do not parse it. |
| **\[New]** MO `control_passed` | `message.custom.message_type` is `"thread_control"` and `type` is `"control_passed"` | You are the new lead. |
| **\[New]** MO `control_taken` | `message.custom.message_type` is `"thread_control"` and `type` is `"control_taken"` | The escalation partner took the lead from you. |
| **\[New]** SR error 131070 | The SR description contains `131070` | Your Service message was rejected. |

Thread control MOs use the same MO template as every other inbound message. These fields differ:

| Field | Value | Notes |
| - | - | - |
| `reference` | `handover-` + 32 characters | Generated by CM, because Meta gives these events no message ID. Built from the WhatsApp Business Account ID, the user's number, the event type and Meta's timestamp, so it is identical when Meta retries the same event. Use it to de-duplicate. |
| `from.number` | The WhatsApp user | The user's phone number, although the user wrote nothing. |
| `message.text` | Short summary | `Thread control passed to <new_owner_role>.` or `Thread control taken by <new_owner_role>.` Never parse it: use `custom`. |
| `message.custom` | Event payload | `message_type` (`thread_control`), `type`, `consumer_phone_number` (as Meta sends it, without `+` or `00`), `timestamp` (Unix seconds, as a string) and a `control_passed` or `control_taken` object. There is no `standby` field. |

> **Do not treat a thread control MO as a customer message**
>
> `from.number` is the WhatsApp user, but the user wrote nothing. Check `custom.message_type` first. Never reply to it, never pass it to a chatbot, and never show it in an agent inbox as something the customer said.

### Example: failed SR with error 131070

Your Service message was rejected because you do not lead the thread, or the thread is idle. Meta's text says "another app" even when nobody leads.

```text
wha: META#131070 - Error sending message in cloud. Message from Meta: (#131070) Message failed to send
because another app is controlling this thread now
```

- Match the number `131070` in the SR description. Do not match the wording: it is Meta's free text and can change.
- Do not retry the Service message. Mark the conversation as not led by you and wait for a signal that you lead again.
- Templates are never rejected for this reason. The escalation partner never gets this error: its Service message takes the thread.

## 7. Examples of each MO

The examples use the default JSON MO template. The envelope (`reference`, `from`, `to`, `time`, `channel`) follows the MO template configured on your account; the thread control information is always in `message.custom`.

### 7.1 MO with standby: false

A user message on a thread you lead. Answer it with a Service message.

```json
{
  "reference": "wamid.HBgLMzE2MzYzNDI2MDUVAgASGBQzQTZCNzU4NzE2NUQ0RTk5MjAzNgA=",
  "from": { "number": "31636342605", "name": "John Doe" },
  "to": { "number": "31612345678" },
  "message": {
    "text": "Can you send me the invoice?",
    "custom": {
      "meta_received_time": "2026-10-28T09:14:02",
      "message_type": "text",
      "standby": false
    }
  },
  "time": "2026-10-28 09:14:02",
  "channel": "WhatsApp"
}
```

### 7.2 MO with standby: true

A copy of a user message while another responder leads. Use it as context only. Do not reply.

```json
{
  "reference": "wamid.HBgLMzE2MzYzNDI2MDUVAgARGBI5QTM4RjhDOEUzOEYxOUYwQzQA",
  "from": { "number": "31636342605", "name": "John Doe" },
  "to": { "number": "31612345678" },
  "message": {
    "text": "Hi, is my order still on the way?",
    "custom": {
      "meta_received_time": "2026-10-28T09:05:11",
      "message_type": "text",
      "standby": true
    }
  },
  "time": "2026-10-28 09:05:11",
  "channel": "WhatsApp"
}
```

The reference is the normal WhatsApp message ID, so only the `standby` flag tells it apart from a message you should answer. You only receive standby MOs when the business has granted you Conversation visibility.

### 7.3 MO with a conversation summary

When you pick up a conversation without having had standby copies of it, Meta can add an AI-generated summary of the conversation so far. It is sent on the first message of a new thread, on a handover (`control_passed`, section 7.4), and on your next message after another responder sent a template during your conversation. You never get both a summary and standby copies for the same conversation.

```json
{
  "reference": "wamid.HBgTTkwuMTk2MzcwNTA0MDkxMTYzNBUUABIYFDNBQUUyMTc4RTlDNjAzQ0I2NkM3AA==",
  "from": { "number": "31636342605", "name": "John Doe" },
  "to": { "number": "31612345678" },
  "message": {
    "text": "Great, thanks. I would like to talk to an employee please",
    "custom": {
      "meta_received_time": "2026-11-06T14:11:57",
      "message_type": "text",
      "standby": false,
      "conversation_context": {
        "type": "summary",
        "summary": {
          "text": "* The customer asked for a product recommendation. ..."
        }
      }
    }
  },
  "time": "2026-11-06 14:11:57",
  "channel": "WhatsApp"
}
```

- Treat `conversation_context` as optional. Do not assume it is present.
- `summary.text` is model-generated prose. Show it to your agent; do not parse it. Order numbers and amounts are kept verbatim, and open items are described as unresolved.

### 7.4 MO thread\_control: control\_passed

Another responder (here: the AI agent) handed the conversation to a role you hold. You are the new lead. Reply with a Service message.

```json
{
  "reference": "handover-7f3a9c0e5b2d4a18f9c7e6b5a4d3c2f1",
  "from": { "number": "31636342605", "name": "" },
  "to": { "number": "31612345678" },
  "message": {
    "text": "Thread control passed to escalation.",
    "custom": {
      "message_type": "thread_control",
      "type": "control_passed",
      "consumer_phone_number": "31636342605",
      "timestamp": "1793178734",
      "control_passed": {
        "previous_owner_role": "ai_agent",
        "new_owner_role": "escalation",
        "metadata": "WhatsApp user requested human agent",
        "conversation_context": {
          "type": "summary",
          "summary": { "text": "* The customer wants to change order 40182. ..." }
        }
      }
    }
  },
  "time": "2026-10-28 09:12:14",
  "channel": "WhatsApp"
}
```

- `metadata` is optional free text (up to 2,000 characters) from the responder that passed the thread.
- `conversation_context` is optional: only included when you had no standby copies of the conversation.
- `timestamp` is Unix seconds, as a string. `1793178734` is 2026-10-28 09:12:14 UTC.

### 7.5 MO thread\_control: control\_taken

The escalation partner took a thread you led. Stop sending Service messages.

```json
{
  "reference": "handover-a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "from": { "number": "31636342605", "name": "" },
  "to": { "number": "31612345678" },
  "message": {
    "text": "Thread control taken by escalation.",
    "custom": {
      "message_type": "thread_control",
      "type": "control_taken",
      "consumer_phone_number": "31636342605",
      "timestamp": "1793179201",
      "control_taken": {
        "previous_owner_role": "customer_service",
        "new_owner_role": "escalation",
        "metadata": "Human agent stepping in"
      }
    }
  },
  "time": "2026-10-28 09:20:01",
  "channel": "WhatsApp"
}
```

- This is the only way you lose the lead with a notification. You are not notified when you release or pass, when the thread goes idle after 24 hours, or when a button tap or ad click routes the thread to another responder.
- If Meta also sends app identifiers (`previous_owner_app_id`, `new_owner_app_id`, `new_owner_business_id`), CM forwards them in the same object. Use the role fields.

## 8. Sending thread control MT Events

You take, pass and release a thread by sending an MT Event to CM, on the same endpoint and with the same product token you use for other WhatsApp MT Events such as mark as read and typing. CM validates the event and sends the action to Meta.

### 8.1 Request

```http
POST https://gw.messaging.cm.com/v1.0/event
Content-Type: application/json
```

Every thread control MT Event has the same body: your product token in `authentication`, your business number in `from`, the WhatsApp user in `to`, the action in `event`, and the `channel`. Only `event` differs per action.

| Field | Required | Description |
| - | - | - |
| `authentication.productToken` | Yes | Your CM product token. |
| `from.number` | Yes | Your WhatsApp business number that the conversation is on, in international format, for example `0031000000001`. |
| `to.number` | Yes | The WhatsApp user. Either the phone number in international format with **digits only** (for example `0031000000002`; no `+`, spaces or dashes), or the user's business-scoped user ID (BSUID), for example `NL.164646464646`. |
| `event.type` | Yes | `ThreadControlTake`, `ThreadControlPass` or `ThreadControlRelease`. |
| `event.custom.target_role` | No, Pass only | The role that should lead next: `ai_agent`, `ctwa`, `customer_service`, `escalation`, `marketing` or `utility`. Use lowercase. Leave it out for an untargeted pass: Meta then passes the thread to the escalation partner, the same as `escalation`. The escalation partner itself must always set it. Ignored for take and release. |
| `channel` | Yes | `WhatsApp` |

### 8.2 Take

The escalation partner takes the thread from whoever leads. The previous lead receives `control_taken` (7.5). You are not notified of your own take.

```json
{
  "authentication": {
    "productToken": "<your product token>"
  },
  "from": {
    "number": "0031000000001"
  },
  "to": {
    "number": "0031000000002"
  },
  "event": {
    "type": "ThreadControlTake"
  },
  "channel": "WhatsApp"
}
```

- Only the escalation partner can take. Meta rejects a take from any other responder (error 2494191).
- Sending a Service message as escalation partner has the same effect (implicit take).

### 8.3 Pass

The lead hands the thread to another role. There are two kinds of pass:

| Pass | `event.custom` | Who becomes the lead |
| - | - | - |
| Targeted | `target_role` set to one of the role identifiers | The responder that holds that role. It receives `control_passed`. |
| Untargeted | No `target_role` | The account's escalation partner. It receives `control_passed`. The escalation partner itself cannot send it. |

### Targeted pass

Hands the thread to the role in `target_role`, here the Meta Business Agent.

```json
{
  "authentication": {
    "productToken": "<your product token>"
  },
  "from": {
    "number": "0031000000001"
  },
  "to": {
    "number": "0031000000002"
  },
  "event": {
    "type": "ThreadControlPass",
    "custom": {
      "target_role": "ai_agent"
    }
  },
  "channel": "WhatsApp"
}
```

### Untargeted pass

Without `target_role`, Meta hands the thread to the account's escalation partner. Only a lead that is not the escalation partner can send it, for example a marketing or utility partner.

```json
{
  "authentication": {
    "productToken": "<your product token>"
  },
  "from": {
    "number": "0031000000001"
  },
  "to": {
    "number": "0031000000002"
  },
  "event": {
    "type": "ThreadControlPass"
  },
  "channel": "WhatsApp"
}
```

- **The escalation partner cannot send an untargeted pass**, because it would pass the thread to itself. Meta rejects it with error 100: `Caller business is an escalation partner; cannot pass thread control to self`. As escalation partner, set `target_role` (for example `ai_agent` to hand back to MBA), or send a release.
- Meta rejects a pass when the thread is idle, when you do not lead it, when the target role is not available on the account (for example `ai_agent` without MBA), when the role has no single primary, or when the target already leads the thread.

### 8.4 Release

The lead gives up the thread. The thread becomes idle and the user's next message is routed again by entry point.

```json
{
  "authentication": {
    "productToken": "<your product token>"
  },
  "from": {
    "number": "0031000000001"
  },
  "to": {
    "number": "0031000000002"
  },
  "event": {
    "type": "ThreadControlRelease"
  },
  "channel": "WhatsApp"
}
```

No event is sent to anyone after a release, so update your own state when you send it. A release on a thread that is already idle succeeds. In every example, `to.number` can also be the user's BSUID, for example `NL.164646464646`.

### 8.5 Response and errors

CM answers the request immediately:

| Response | Meaning |
| - | - |
| HTTP 200 `{"details": "", "errorCode": 0}` | CM accepted the event and will send it to Meta. |
| `errorCode` 301, details `Missing from` | `from.number` is missing. |
| `errorCode` 302, details `Missing to` | `to.number` is missing. |
| `errorCode` 304, details `Missing body` or `Missing body/eventType` | `event` or `event.type` is missing. |
| `errorCode` 305, details `Missing channel` or `Unknown channel` | `channel` is missing or unknown. |

> **Accepted is not the same as applied**
>
> A 200 response means CM accepted the event, not that Meta applied it. There is **no status report** for a thread control MT Event. If the event is invalid (for example a `target_role` that is not one of the roles above, or a `to.number` with a `+`) or Meta rejects it (for example a take from a responder that is not the escalation partner, an untargeted pass from the escalation partner, or a pass on an idle thread), you are not notified.
>
> Confirm the result from the next signals instead: after a successful take, your next MO has `standby: false` and your Service messages are delivered; after a successful pass or release, your next MO (if any) has `standby: true`, and a Service message from you fails with 131070.

## 9. Prerequisites

### 9.1 On the WhatsApp account (Meta)

- The account has **more than one responder** that can answer an inbound message, for example CM and the Meta Business Agent, or CM and another partner.
- The business has configured **Conversation Routing** in Meta Business Suite (which responder is primary for each entry point).
- The business has accepted the **Meta Business Agents and Platform Terms of Service**. Without it, Conversation visibility, the escalation partner and thread control are not available.

### 9.2 Meta Business Suite settings (made by the business)

| Setting | What it does |
| - | - |
| Message routing | Maps each entry point (service, marketing replies, utility replies, Click-to-WhatsApp) to its primary responder. Optionally, replies to a template go to the responder that sent it (Route to Sender). |
| Thread control | Designates the escalation partner: at most one per account. |
| Conversation visibility | Lets responders that do not lead receive copies of the user's messages (MOs with `standby: true`). Off by default. Without it you receive nothing while you are in standby, but you may receive a `conversation_context` summary when a conversation comes to you. |

### 9.3 At CM

- Nothing to subscribe to. CM receives Meta's routing and handover webhooks and forwards them on your existing MO webhook.
- Thread control MT Events use your existing product token and the MT Event endpoint (section 8).

## 10. Implementation

Follow these steps in order. Steps 3 and 4 depend on your situation; the other steps apply to both.

### Step 1. Read message.custom on every MO

Do not decide on `message.text` alone. Branch in this order:

1. `message_type` is `"thread_control"`: update your thread state (`control_passed`: you lead, `control_taken`: you no longer lead). Never reply, never route it to a chatbot or an inbox.
2. `standby` is `true`: another responder leads. Store as context. Never reply.
3. Everything else: a regular message on a thread you lead. Handle it as a customer message. If `conversation_context` is present, show the summary to your agent.

### Step 2. Send thread control MT Events

Add take, pass and release to your integration with the MT Event endpoint and your product token (section 8).

### Step 3. For the Meta Business Agent

Ask the business to configure your CM integration as the **escalation partner**. Then follow section 4: treat standby MOs as context, use `control_passed` (or a take) to become the lead, send your MTs, and release or pass the thread back to `ai_agent` when you are done.

### Step 4. For several partners

Agree with the business which entry points your integration is primary for. The business sets this up in Meta Business Suite under Conversation Routing. Then read `standby` on every MO to know your position (section 5.3).

> **For both situations**
>
> Ask the business to grant your integration **Conversation visibility** if you want copies of the user's messages while another responder leads. Without it you receive no MOs with `standby: true`.

### Step 5. Track state and handle errors

- Keep the thread state per conversation, keyed by the user's phone number: thread control MOs identify the user by phone number only. Meta has no endpoint that returns the current lead.
- Never send a Service message when you do not lead, unless you are the escalation partner and want to take the thread. Templates are always allowed.
- On a failed SR with 131070, do not retry. Mark the conversation as not led by you. Match the number, not Meta's wording.
- When you send a release or a pass, set your state to "not leading" yourself: no event confirms it.
- After 24 hours without a user message the thread is idle and nothing tells you. Clear your state.
- De-duplicate thread control MOs on `reference`, and ignore events whose `custom.timestamp` is older than the last one you processed for that user.

### Step 6. Test your setup

| # | Trigger | Check |
| - | - | - |
| 1 | A user writes while another responder leads. | You get an MO with `standby: true` and do not reply. |
| 2 | The other responder passes the thread to you. | You get a `control_passed` MO, set your state to lead and can send a Service message. |
| 3 | The escalation partner takes a thread you lead. | You get a `control_taken` MO and stop sending Service messages. |
| 4 | You send a Service message without leading (and you are not the escalation partner). | The SR fails with 131070 and you do not retry. |
| 5 | You send `ThreadControlPass` with `target_role: ai_agent`. | MBA answers the user's next message. If you have Conversation visibility, you get it as `standby: true`. |
| 6 | You send `ThreadControlRelease`. | No MO follows. The user's next message is routed by entry point. |
| 7 | As escalation partner, you send `ThreadControlTake` while MBA leads. | The user's next message reaches you with `standby: false`. |
