Conversation routing and thread control
A WhatsApp number can be shared with several providers. Meta's Conversation routing (set in Meta Business Suite) then decides which provider owns each conversation. Only the owner can send regular replies (Service messages) and reactions. Everyone else gets Meta error 131070. Templates always work, whoever owns the conversation, and they never change the owner.
This page covers the API side: how to detect the problem, how to read the routing status of a number, and how to take over and hand back conversations when SendSeven is the escalation partner. For the setup in Meta Business Suite, see Troubleshooting: conversation routing.
How ownership works
- A conversation that has been quiet for 24 hours is idle and has no owner. The next message from the customer goes to the provider that is primary for that entry point (Service, Click to WhatsApp, Utility Response, Marketing Message Response, Calls).
- A Click-to-WhatsApp ad click or a reply to a marketing or utility template can move the conversation to the provider that is primary for that entry point.
- A number can have at most one escalation partner. The escalation partner can take over any conversation, and the current owner can pass a conversation to another provider or release it.
- Meta has no API that reports the current owner of a conversation. SendSeven tracks ownership from send results and from Meta's handover events, so the state below is SendSeven's best knowledge, not a live lookup.
Detecting the problem
When another provider owns the conversation, a regular reply fails. The message ends up with status failed:
{
"id": "msg_abc123",
"status": "failed",
"error_code": "131070",
"error_message": "Message failed to send because another app is controlling this thread now.",
"meta": {
"error_reason": "thread_controlled_by_other_app"
}
}
GET /api/v1/messages/{message_id}returnserror_code: "131070"andmeta.error_reason: "thread_controlled_by_other_app".- The
message.failedwebhook carries the same values indata.message.error_codeanddata.message.meta.error_reason. - Reactions return
success: falsewitherror_code: "131070"anderror_reason: "thread_controlled_by_other_app"(see Reactions).
Check error_reason rather than parsing the error text. The text comes from Meta and can change.
Routing status of a number
GET /api/v1/whatsapp/channels/{channel_id}/conversation-routing?refresh=false
Required scope: channels:read
{
"status": "enabled",
"roles": ["escalation"],
"is_standby": false,
"is_escalation_partner": true,
"checked_at": "2026-11-02T10:00:00Z"
}
| Field | Meaning |
|---|---|
status | enabled: conversation routing is on for the number. not_enabled: it is off, and SendSeven receives and sends all messages. unknown: not checked yet. error: the last check failed. |
roles | The roles SendSeven holds for the number, as WhatsApp reports them, for example customer_service, ctwa, marketing, utility, escalation, ai_agent or calls. An empty list with status: "enabled" means another provider handles these conversations. |
is_standby | true when SendSeven receives messages in standby while another provider controls the conversation. |
is_escalation_partner | true when SendSeven is the escalation partner and can take over conversations. |
checked_at | When SendSeven last checked the roles with WhatsApp. |
SendSeven checks the roles when you connect the number, regularly afterwards, and after a send fails with 131070. Pass refresh=true to check again now, for example right after you changed the routing in Meta Business Suite. Live checks are limited to one per minute per channel; a faster refresh=true returns the stored value.
The same object is also part of GET /api/v1/whatsapp/channels/{channel_id}/account-status as conversation_routing. That endpoint always returns the stored value and never triggers a live check.
Thread control of a conversation
Get the state
GET /api/v1/conversations/{conversation_id}/whatsapp/thread-control
Required scope: conversations:read
{
"conversation_id": "conv_123",
"channel_id": "ch_456",
"controlled_by_other_app": true,
"controlled_by_us": false,
"source": "send_failed",
"updated_at": "2026-11-02T10:00:00Z",
"taken_over_at": null,
"taken_over_by_user_id": null,
"can_take_over": true,
"can_hand_back": false,
"take_over_unavailable_reason": null,
"routing": {
"status": "enabled",
"roles": ["escalation"],
"is_standby": false,
"is_escalation_partner": true,
"checked_at": "2026-11-02T10:00:00Z"
}
}
| Field | Meaning |
|---|---|
controlled_by_other_app | Another provider controls the conversation. Regular replies and reactions fail until that changes. |
controlled_by_us | SendSeven took over the conversation or another provider passed it to SendSeven. |
source | What last changed the state: send_failed, send_succeeded, control_taken, control_passed, take_over or hand_back. |
taken_over_at, taken_over_by_user_id | When and by which team member SendSeven last took over the conversation. |
can_take_over | true when SendSeven is the escalation partner and does not control the conversation yet. |
take_over_unavailable_reason | Why can_take_over is false: not_escalation_partner, conversation_routing_not_enabled, routing_unknown or already_controlled_by_us. null otherwise. |
can_hand_back | true when SendSeven controls the conversation. |
routing | The routing status of the conversation's number. |
A WhatsApp conversation without any recorded change returns all flags as false. A conversation on another channel returns 422 with not_whatsapp_conversation.
Take over a conversation
POST /api/v1/conversations/{conversation_id}/whatsapp/take-over
Content-Type: application/json
{"metadata": "Escalated by support"}
Required scopes: conversations:update and messages:create (both). Taking over only makes sense so you can reply, so a token that can't send messages can't take a conversation over. Handing back only needs conversations:update.
The body is optional. metadata is free text of up to 2000 characters that WhatsApp passes on to the other provider's app. The other provider can read it, so never put personal data or secrets in it.
Taking over only works when SendSeven is the escalation partner for the number. Afterwards the other provider can't reply until you hand the conversation back, or until the customer has not written for 24 hours and the conversation returns to your normal routing. The response is the updated thread-control state. SendSeven adds a note to the conversation saying who took it over.
Hand a conversation back
POST /api/v1/conversations/{conversation_id}/whatsapp/hand-back
Content-Type: application/json
{"mode": "pass", "target_role": "customer_service"}
Required scope: conversations:update
| Field | Default | Meaning |
|---|---|---|
mode | pass | pass hands the conversation to the provider with target_role. release makes the conversation idle, so your normal routing picks the next owner when the customer writes again. |
target_role | customer_service | One of customer_service, ctwa, marketing, utility, escalation, ai_agent. Used with pass only. |
metadata | null | Free text of up to 2000 characters that WhatsApp passes on to the other provider's app. The other provider can read it, so never put personal data or secrets in it. |
The body is optional; an empty request passes the conversation to the primary provider for customer service. As the escalation partner, SendSeven always hands back to a specific role. The response is the updated thread-control state.
Errors
Thread-control errors return a structured detail:
{
"detail": {
"code": "not_escalation_partner",
"message": "SendSeven isn't the escalation partner for this number."
}
}
| Status | detail.code | Meaning |
|---|---|---|
| 404 | (plain detail) | The conversation does not exist in this workspace, or you can't access it. |
| 409 | not_escalation_partner | SendSeven isn't the escalation partner for this number, so it can't take over. Select it in Meta Business Suite under Conversation routing. |
| 409 | conversation_routing_not_enabled | Conversation routing isn't set up for this number. |
| 409 | not_thread_owner | SendSeven doesn't control the conversation, so it can't hand it back. |
| 409 | channel_unavailable | The WhatsApp channel is disconnected or can't be used right now. |
| 422 | not_whatsapp_conversation | The conversation is not a WhatsApp conversation. |
| 422 | invalid_target_role | target_role is not one of the allowed roles. |
| 429 | rate_limited | Another take-over or hand-back for this conversation ran a few seconds ago. Wait about 5 seconds and try again. |
| 502 | provider_error | WhatsApp didn't accept the request. Try again in a moment. |
Real-time updates
The SendSeven app updates the conversation as soon as the state changes, for example when another provider takes a conversation back. In your own integration, read the thread-control state again after a send fails with thread_controlled_by_other_app, and after you take over or hand back.
Next steps
- Troubleshooting: conversation routing: fixing the setup in Meta Business Suite
- Sending templates: templates work whoever controls the conversation
- Webhook events: how failed messages reach your system