BrightshoalDocsOpen console
MenuLet your team take over

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" } }'
Response
{
  "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.

Terminal
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"
  }'
Response 201
{ "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:

Terminal
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" } }'
Response
{ "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.

Terminal
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

StatusCodeWhat happened
409live_handoff_disabledLive handoff is off for your organization.
409conversation_endedThe conversation closed. Takeover and replies need an open one.
409already_taken_overSomeone else holds the seat. The body names them in taken_over_by.
409not_taken_overNobody holds the seat: you never took it, or it lapsed. Take it over again.
409seat_changedThe seat changed hands while you released it. Read the conversation and decide again.
409client_message_id_reusedThat client_message_id was already used for different text.
409whatsapp_reply_window_closedOn WhatsApp you can only reply within 24 hours of the visitor's last message.
403forbiddenThe seat is held by a different agent.id from the one you sent.

Events to listen for

EventFires when
conversation.handoff_requestedThe assistant asks for a person.
conversation.takeover.startedA person takes over, from the console (source: "console") or the API (source: "api").
conversation.takeover.releasedThe seat is released: reason is released, forced, conversation_ended or abandoned (three minutes without a heartbeat or message).
conversation.endedThe 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.

Stuck on let your team take over?