After the conversation
Let your team take over
Take a live chat over from the assistant, reply as a person from your own tools, keep the seat while you type, and hand the conversation back.
When a visitor needs a person, someone on your team can take over the conversation. While they hold it, the assistant stops replying and the visitor sees their messages instead. The console does this out of the box; this guide does the same from your own helpdesk, inbox or CRM.
Takeover works on open chat conversations, on your website and on WhatsApp. It needs live handoff to be on for your organization (it is unless you turned it off in the console). With it off, takeover, heartbeat and messages as a person answer 409 live_handoff_disabled; release still works.
Know when someone is needed
Subscribe a webhook to conversation.handoff_requested. It fires when the assistant decides the visitor should talk to a person, with the reason, the last 20 messages of the transcript and the lead_id if there is one. To see what is waiting right now, list open conversations with GET/api/conversations.
Take the seat
Call POST/api/conversations/{id}/takeover with an agent: who is replying. Use the same agent.id on every call that follows, because the seat belongs to that id.
curl -X POST https://api.myleadsfactory.tech/api/conversations/conv_0c8e5f2a9b7d4c1e86f3a0d2b4c6e8f1/takeover \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "agent": { "id": "agent_maria", "name": "Maria", "email": "maria@acmetravel.com" } }'{
"data": {
"id": "conv_0c8e5f2a9b7d4c1e86f3a0d2b4c6e8f1",
"status": "open",
"taken_over_at": "2026-10-28T09:35:12.004Z",
"taken_over_heartbeat_at": "2026-10-28T09:35:12.004Z",
"taken_over_by": "agent_maria"
}
}If you send no agent, the seat is held as api_key, and every operator using that key shares it. Taking a seat you already hold succeeds again without a second event.
Reply as a person
Send each message with POST/api/conversations/{id}/messages, with the same agent. The visitor sees it at once, signed with the agent's name when you send one.
curl -X POST https://api.myleadsfactory.tech/api/conversations/conv_0c8e5f2a9b7d4c1e86f3a0d2b4c6e8f1/messages \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Hi Ana, Maria here. I can hold two seats on the 14 May flight for you.",
"agent": { "id": "agent_maria", "name": "Maria" },
"client_message_id": "helpdesk-7781-3"
}'{ "data": { "id": "msg_...", "role": "agent", "content": "Hi Ana, Maria here. ...", "seq": 9, "created_at": "2026-10-28T09:35:40.118Z" } }content is 1 to 4,000 characters. Set client_message_id (up to 200 characters, unique per message) so a retry after a timeout returns the message already sent instead of sending it twice.
The visitor's replies arrive as conversation.message.created webhooks with role: "user", and in GET/api/conversations/{id}.
Keep the seat
A seat with no heartbeat and no message for three minutes is released, and the assistant answers the visitor's next message again. Sending a message counts, but an operator who is reading or typing sends nothing, so while they have the conversation open, call POST/api/conversations/{id}/takeover/heartbeat about once a minute with the same agent:
curl -X POST https://api.myleadsfactory.tech/api/conversations/conv_0c8e5f2a9b7d4c1e86f3a0d2b4c6e8f1/takeover/heartbeat \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "agent": { "id": "agent_maria" } }'{ "data": { "taken_over_heartbeat_at": "2026-10-28T09:36:12.330Z" } }Hand it back
When your operator is done, release the seat with POST/api/conversations/{id}/release, again with the same agent. The assistant replies to the visitor's next message.
curl -X POST https://api.myleadsfactory.tech/api/conversations/conv_0c8e5f2a9b7d4c1e86f3a0d2b4c6e8f1/release \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "agent": { "id": "agent_maria" } }'The response is the same summary as takeover, with the seat fields back to null. An API key can also release a seat someone else holds; that is recorded as a forced release.
When the API says no
| Status | Code | What happened |
|---|---|---|
| 409 | live_handoff_disabled | Live handoff is off for your organization. |
| 409 | conversation_ended | The conversation closed. Takeover and replies need an open one. |
| 409 | already_taken_over | Someone else holds the seat. The body names them in taken_over_by. |
| 409 | not_taken_over | Nobody holds the seat: you never took it, or it lapsed. Take it over again. |
| 409 | seat_changed | The seat changed hands while you released it. Read the conversation and decide again. |
| 409 | client_message_id_reused | That client_message_id was already used for different text. |
| 409 | whatsapp_reply_window_closed | On WhatsApp you can only reply within 24 hours of the visitor's last message. |
| 403 | forbidden | The seat is held by a different agent.id from the one you sent. |
Events to listen for
| Event | Fires when |
|---|---|
conversation.handoff_requested | The assistant asks for a person. |
conversation.takeover.started | A person takes over, from the console (source: "console") or the API (source: "api"). |
conversation.takeover.released | The seat is released: reason is released, forced, conversation_ended or abandoned (three minutes without a heartbeat or message). |
conversation.ended | The conversation closes. handoff_status says whether someone answered in time (answered), never took over (missed) or took over and left the visitor waiting (abandoned). |
Full payloads are in webhook events.