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.
First operator wins
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: startdrafts sendrelease.

Takeover helpers are rolling out
The 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.
python
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 💚")
The session object
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:

json
{
  "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.
  • response is null — 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.
These keys only appear during a takeover
On a normal turn, none of 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.
python
# 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 reply

REST 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.

POST/api/sdk/takeover/{user_id}auth required

Open (or return the existing) takeover session. First operator wins; fires takeover.started only on a genuine open.

Parameters
  • 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).
Example
bash
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" }'
Response
json
{
  "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
}
GET/api/sdk/takeover/{user_id}auth required

Fetch the active session, or { session: null } if none.

Example
bash
curl -H "X-API-Key: hx_..." https://humaneai.vaarak.com/api/sdk/takeover/u_8f2
POST/api/sdk/takeover/{user_id}/messagesauth required

Inject an operator-authored turn. 409 if no active session. Fires takeover.message.

Parameters
  • bodystringbody · reqThe operator's message text (1–10000 chars).
Example
bash
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." }'
Response
json
{ "message_id": "9a…", "created_at": "2026-06-02T14:22:30Z" }
POST/api/sdk/takeover/{user_id}/releaseauth required

Hand the conversation back to the AI. 409 if no active session. Fires takeover.released.

Parameters
  • handback_messagestringbodyOptional message to deliver on hand-back.
Example
bash
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." }'
POST/api/sdk/takeover/{user_id}/draftsauth required

AI co-pilot: propose short reply options for the operator. Does not require an active session; returns { drafts: [] } rather than erroring if the model hiccups.

Parameters
  • countintbodyHow many distinct options to return (1–6, default 3).
  • guidancestringbodyOptional steer, e.g. 'keep it brief'.
Example
bash
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" }'
Response
json
{ "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.

EventFires when
takeover.startedA new session is opened (not on a no-op re-start).
takeover.messageAn operator injects a turn. Adds body + message_id.
takeover.releasedThe session is handed back to the AI.
takeover.expiredThe idle window elapsed and the session auto-released.

Full payload shapes are in the Webhook Events reference.