Concepts
Human Takeover
Sometimes the right answer is a real person. Human takeover lets one of your operators step into a live AI conversation: the bot pauses its AI, the operator sends messages the end-user sees as one continuous assistant voice, and when they're done they hand the thread back to the AI — no context lost.
The concept
A takeover is a session over one end-user's conversation. While it's active, Humane suppresses the AI for that user and delivers the operator's messagesinstead. The end-user doesn't see a hand-off seam — to them it's the same assistant, now being driven by a human. The lifecycle is deliberately small:
- start — an operator claims the conversation (first operator wins).
- drafts — optional: ask the AI co-pilot for reply suggestions.
- send — inject an operator-authored turn into the live thread.
- release — hand the conversation back to the AI.
start is idempotent per user: if a session is already active, a second start returns the existing one rather than opening a competing session. Sessions also auto-release after an idle window (configurable via idle_minutes) so a forgotten takeover doesn't silence the bot forever.The SDK flow
The Python SDK exposes the whole lifecycle under client.takeover: start → drafts → send → release.
client.takeover Python helpers below are shipping incrementally. The REST endpoints under /api/sdk/takeover are live today and drive the exact same lifecycle — reach for those while the typed wrappers land.from humane_ai import HumaneClient
with HumaneClient(api_key="hx_...") as client:
# 1. An operator steps in. Returns the active TakeoverSession.
session = client.takeover.start(
user_id="u_8f2",
operator_id="op_1",
operator_name="Dana",
operator_avatar_url=None, # optional
idle_minutes=30, # optional auto-release window
)
# 2. Ask the AI co-pilot for reply options (best-effort; may return []).
options = client.takeover.drafts("u_8f2", count=3, guidance="keep it warm")
# 3. Send an operator-authored turn — the end-user sees this as the assistant.
client.takeover.send("u_8f2", body=options[0] if options else "Hey, it's Dana — I'm here.")
# Is a session still active? (None when no operator is steering.)
active = client.takeover.get("u_8f2") # TakeoverSession | None
# 4. Hand back to the AI.
client.takeover.release("u_8f2", handback_message="Handing you back to the assistant 💚")start, get, and release return a TakeoverSession with id, end_user_id, operator_id, operator_name, state ("active" → "released"), and ISO timestamps started_at / idle_release_at / released_at. drafts() returns a plain list of suggestion strings (or [] if the co-pilot hiccups — it never raises on you).The /process contract during takeover
Here's the part that matters for your bot. When a takeover is active and a new user message arrives, your /process call does not generate an AI reply. Instead the response carries three takeover-specific keys, and your bot must render the operator's messages instead of generating anything:
{
"response": null,
"suppress_ai": true,
"takeover": {
"session_id": "1f2e…",
"state": "active",
"operator_id": "op_1",
"operator_name": "Dana",
"started_at": "2026-06-02T14:22:10Z",
"idle_release_at": "2026-06-02T14:52:10Z"
},
"pending_operator_messages": [
{ "message_id": "9a…", "body": "Hey, it's Dana — I'm here.", "created_at": "2026-06-02T14:22:30Z" }
],
"user": {
"id": "u_8f2",
"mood": 0.31, "energy": 0.42, "trust": 0.55,
"sentiment": 0.38, "familiarity": 0.60,
"interaction_count": 7
}
}The contract, precisely:
suppress_ai: true— the signal to your bot: do not generate an AI reply for this turn.responseisnull— no AI ran, so there is nothing to show from the model.takeover— who is steering and since when. Render an "a human is replying" affordance if you like.pending_operator_messages— the operator turns to deliver to the end-user, in order. Render these instead of an AI reply.
suppress_ai, takeover, or pending_operator_messages are present and response is the AI reply as usual. So a safe integration is: if suppress_ai is truthy, deliver pending_operator_messages; otherwise show response.The user's inbound message is still persisted to history either way, so the operator sees it in their feed. The safety / timing / analysis blocks are deliberately omitted during takeover — no AI pipeline ran.# Inside your bot's message handler:
r = client.process(user_id="u_8f2", message=incoming_text)
raw = r.raw # the full JSON dict
if raw.get("suppress_ai"):
for m in raw.get("pending_operator_messages", []):
deliver_to_user(m["body"]) # render the operator's words as the assistant
else:
deliver_to_user(r.response) # normal AI replyREST endpoints (non-Python)
Not on Python? The lifecycle is four endpoints under /api/sdk, all authenticated with X-API-Key. The {user_id} path segment is your external end-user id — the same one you pass to observe / process.
/api/sdk/takeover/{user_id}auth requiredOpen (or return the existing) takeover session. First operator wins; fires takeover.started only on a genuine open.
- operator_idstringbody · reqYour id for the operator.
- operator_namestringbody · reqDisplay name shown to the end-user.
- operator_avatar_urlstringbodyOptional avatar URL.
- idle_minutesintbodyAuto-release after this many idle minutes (1–10080).
curl -X POST https://humaneai.vaarak.com/api/sdk/takeover/u_8f2 \
-H "Content-Type: application/json" -H "X-API-Key: hx_..." \
-d '{ "operator_id": "op_1", "operator_name": "Dana" }'{
"id": "1f2e…",
"end_user_id": "8c…",
"operator_id": "op_1",
"operator_name": "Dana",
"state": "active",
"started_at": "2026-06-02T14:22:10Z",
"idle_release_at": "2026-06-02T14:52:10Z",
"released_at": null
}/api/sdk/takeover/{user_id}auth requiredFetch the active session, or { session: null } if none.
curl -H "X-API-Key: hx_..." https://humaneai.vaarak.com/api/sdk/takeover/u_8f2/api/sdk/takeover/{user_id}/messagesauth requiredInject an operator-authored turn. 409 if no active session. Fires takeover.message.
- bodystringbody · reqThe operator's message text (1–10000 chars).
curl -X POST https://humaneai.vaarak.com/api/sdk/takeover/u_8f2/messages \
-H "Content-Type: application/json" -H "X-API-Key: hx_..." \
-d '{ "body": "Hey, it'\''s Dana — I'\''m here." }'{ "message_id": "9a…", "created_at": "2026-06-02T14:22:30Z" }/api/sdk/takeover/{user_id}/releaseauth requiredHand the conversation back to the AI. 409 if no active session. Fires takeover.released.
- handback_messagestringbodyOptional message to deliver on hand-back.
curl -X POST https://humaneai.vaarak.com/api/sdk/takeover/u_8f2/release \
-H "Content-Type: application/json" -H "X-API-Key: hx_..." \
-d '{ "handback_message": "Handing you back to the assistant." }'/api/sdk/takeover/{user_id}/draftsauth requiredAI co-pilot: propose short reply options for the operator. Does not require an active session; returns { drafts: [] } rather than erroring if the model hiccups.
- countintbodyHow many distinct options to return (1–6, default 3).
- guidancestringbodyOptional steer, e.g. 'keep it brief'.
curl -X POST https://humaneai.vaarak.com/api/sdk/takeover/u_8f2/drafts \
-H "Content-Type: application/json" -H "X-API-Key: hx_..." \
-d '{ "count": 3, "guidance": "reassure them about pricing" }'{ "drafts": ["I totally get the concern…", "Happy to walk you through it…", "No pressure at all…"] }Webhook events
Every lifecycle step fires a webhook so your other systems (and the dashboard live-feed) learn of it. Each payload carries your external user_id so you can correlate without an id mapping.
| Event | Fires when |
|---|---|
| takeover.started | A new session is opened (not on a no-op re-start). |
| takeover.message | An operator injects a turn. Adds body + message_id. |
| takeover.released | The session is handed back to the AI. |
| takeover.expired | The idle window elapsed and the session auto-released. |
Full payload shapes are in the Webhook Events reference.