The messaging_handovers field tells an app when ownership of a WhatsApp conversation thread changes under Meta's Conversation Routing. When several responders share one business phone number (for example your app, another partner's app, and Meta Business Agent), exactly one owns each thread. Each notification names the role that held the thread before and the role that holds it now.
What messaging_handovers Reports
There are two notification types, named in value.type:
| Type | Delivered to | Meaning |
|---|---|---|
control_passed | The new owner | The current owner passed the thread to you through the Thread control API. You are now expected to reply. |
control_taken | The previous owner | The designated escalation partner took the thread. Stop replying to the user. |
Role identifiers used in the owner fields are customer_service, marketing, utility, ctwa, ai_agent, and escalation.
When It Fires
control_passedfires when the thread owner calls pass on the Thread control API.control_takenfires when the designated escalation partner takes the thread, either explicitly through the Thread control API or implicitly by sending a Service message.
A release names no new owner, so it produces no notification. The next responder is chosen by entry-point routing when the user sends their next message. A thread also returns to idle on its own after 24 hours of user inactivity, without a notification.
Payload Shape
Every notification uses the same envelope:
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "<WABA_ID>",
"changes": [
{
"field": "messaging_handovers",
"value": {
"messaging_product": "whatsapp",
"sender": { "phone_number": "<WHATSAPP_USER_PHONE_NUMBER>" },
"recipient": {
"phone_number_id": "<PHONE_NUMBER_ID>",
"display_phone_number": "<BUSINESS_PHONE_NUMBER>"
},
"type": "control_passed",
"timestamp": "<UNIX_SECONDS>",
"control_passed": {
"previous_owner_role": "ai_agent",
"new_owner_role": "escalation",
"metadata": "WhatsApp user requested human agent",
"conversation_context": {
"type": "summary",
"summary": { "text": "AI-generated summary string" }
}
}
}
}
]
}
]
}
object, entry[].id, field, messaging_product, recipient.phone_number_id, recipient.display_phone_number, type, and timestamp are always present. sender.phone_number and everything inside the control_passed or control_taken object are optional.
control_passed
| Field | Notes |
|---|---|
previous_owner_role | Role of the responder that passed control. |
new_owner_role | Role of the responder that received control. |
new_owner_app_id | Included only when the new owner is identified by app rather than by business. |
metadata | The free-form string from the pass request, up to 2,000 characters. |
conversation_context | Optional AI-generated summary. Not sent if you already receive standby events for the conversation. |
previous_owner_app_id, previous_owner_app_role | Deprecated and slated for removal. previous_owner_app_role only ever holds meta_business_agent. Use the role fields instead. |
control_taken
| Field | Notes |
|---|---|
previous_owner_role | Role of the responder that lost control. |
new_owner_role | Always escalation, because only the designated escalation partner can take a thread. |
new_owner_app_id | Included only when the escalation partner is identified by app. |
metadata | For an explicit take, the caller's string (up to 2,000 characters). For an implicit take, Control taken via service message. |
previous_owner_app_id | Deprecated. Absent when a partner loses the thread. control_taken has no previous_owner_app_role. |
Dualhook Handling
Dualhook subscription: not subscribed. Dualhook's Meta app does not subscribe to messaging_handovers, so a Dualhook connection never delivers it to you, even though Meta allows Webhook Override for this field. Dualhook has no Thread control or Conversation Routing features, and the Runtime API has no thread control route.
To use Conversation Routing, configure routing in Meta Business Suite and subscribe your own Meta app to messaging_handovers before routing applies to the number.
Enabling Meta Business Agent creates or changes a routing configuration even if you never set one up. The agent becomes the primary responder for new conversations and previous primary responders move to standby, so on a Dualhook connection those conversations stop reaching your endpoint. See AI agents on WhatsApp Business.
Implementation Notes
- Key ownership state on
previous_owner_roleandnew_owner_role, not on the deprecated app ID fields. - Update ownership from these notifications. Meta has no endpoint that reports the current thread owner.
- Deduplicate on
recipient.phone_number_id,type,timestamp, and the user. Those first three alone are not unique, becausetimestampis in whole seconds and two users can hand over in the same second. - Identify the user from
sender.phone_numberwhen present, otherwise from the identifier you already track for the thread. A phone number and a BSUID for the same user are one person. - If you cannot identify the user, apply the update idempotently instead of dropping it.