Skip to main content
POST
/
v1
/
v2
/
conversations
/
{conversation_id}
/
end_session
End a v2 conversation through the close pipeline (admin)
curl --request POST \
  --url https://api-sandbox.featherhq.com/v1/v2/conversations/{conversation_id}/end_session \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <api-key>' \
  --data '
{
  "end_user_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
  "session_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
  "agent_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
  "channel": "api",
  "reason": "<string>"
}
'
{
  "session_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
  "messages": [
    {
      "content": "<string>",
      "turn_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
      "sequence": 0,
      "producing_assistant_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
    }
  ],
  "handoff": {},
  "call_transfer": {
    "destination": "<string>",
    "mode": "cold",
    "label": "<string>",
    "connecting_text": "<string>",
    "reason": "<string>",
    "warm_destinations": [
      "<string>"
    ],
    "max_retries": 1,
    "retry_text": "<string>",
    "on_failure": "<string>",
    "warm": {
      "message_mode": "ai_summary",
      "briefing_template": "<string>",
      "max_dial_seconds": 30,
      "max_briefing_seconds": 60,
      "bridge_confirm_seconds": 20,
      "max_hold_seconds": 110,
      "hold_announcement_text": "One moment — I'll place you on a brief hold while I connect you.",
      "reassurance_interval_seconds": 20,
      "reassurance_text": "Thanks for your patience — I'm still working on connecting you.",
      "detect_supervisor_voicemail": false,
      "detect_supervisor_silence": false,
      "silence_reminder_text": "Hello — are you available to take this call?",
      "silence_timeout_seconds": 15,
      "retry_announcement_text": "I couldn't reach them that way — let me try connecting you directly.",
      "failure_text": "I'm sorry, I wasn't able to connect you right now. I can keep helping you here."
    },
    "warm_overrides": {
      "briefing_template": "<string>"
    }
  },
  "dtmf_input": {
    "expects_input": true,
    "max_digits": 32,
    "termination_digit": "<string>",
    "inter_digit_timeout_ms": 30125,
    "prefix": "<string>"
  },
  "session_status": "<string>",
  "error": "<string>",
  "model_used": "<string>",
  "token_count": 123,
  "latency_ms": 123
}

Authorizations

x-api-key
string
header
required

Path Parameters

conversation_id
string<uuid>
required

Body

application/json

Admin-triggered runtime end-session.

Distinct from POST /v1/sessions/{id}/close which only flips the DB status. This drives the full Runtime.end_session pipeline (AOP cleanup, memory flush, OTel session.end span, triggered_by=admin).

session_id is required: Runtime.from_authenticated falls through to get_or_create_active_session when it's missing, which would mint a fresh ACTIVE session purely to end it on the same request. Requiring the caller to name the target session prevents that ghost-session path and forces a clear 422 instead of a misleading 200.

farewell_text is intentionally omitted: the runtime ignores it under PersistStrategy.PER_TURN (used by every HTTP channel today), so accepting it would silently drop the value. Callers that want a user-visible farewell should send a final POST /v1/runtime/turn with the goodbye message before calling this endpoint.

end_user_id
string<uuid>
required
session_id
string<uuid>
required
agent_id
string<uuid> | null
channel
string
default:api
reason
string | null

Response

Successful Response

Universal post-turn snapshot returned by Runtime.run_turn.

Every channel sees the same fields:

  • session_id: the session this turn ran against. Always populated when a real Runtime produced the result (the caller may have passed session_id=None and asked the runtime to resolve / create the active session — this is how it learns which one was used). Optional in the type for the benefit of test fixtures that mint TurnResults by hand without a Runtime.
  • messages: 1 message for API/voice; N (Plan 5) for SMS chunked.
  • status: snapshot of Runtime._status at end-of-turn. The caller uses this to decide whether to call end_session.
  • handoff: populated when AOP reached a HandoffNode. The dict shape mirrors the existing AOPExecutionResult.handoff_config for forward-compat — Plan 3 does not re-type it.
  • error: when AOP execution fails, this is the AOPExecutionResult.execution_error string the pipeline stamped (e.g. "node_execution_failed: ValidationError: ...", "routing_error: <node_id>", "unknown_node_type"); falls back to the legacy "max_steps_exceeded" label only when the pipeline didn't attach a reason. repr(exc) for an uncaught exception in _execute. None on success / handoff / policy-violation.
  • model_used / token_count / latency_ms: aggregate metrics surfaced for telemetry; None when the turn made no LLM call.
status
enum<string>
required

In-memory lifecycle status of a Runtime instance.

Distinct from ConversationSession.status (DB column). The DB status is the durable end-state of a session row; this enum is the per-instance live signal that channel runtimes consult after each run_turn to decide whether to call end_session.

Available options:
idle,
running_turn,
end_requested,
end_processing,
ended,
error
session_id
string<uuid> | null
messages
AssistantMessage · object[]
handoff
Handoff · object
call_transfer
CallTransferIntent · object

The resolved, ready-to-execute transfer the voice host renders after a turn.

Both authoring surfaces converge on this: the assistant transfer_call platform tool (label → destination resolved from config) and the workflow HandoffNode voice transfer (deterministic single target, or LLM-picked among many) both produce a CallTransferIntent; the voice worker reads it via VoiceRuntime.pending_transfer and issues the SIP REFER. connecting_text is the optional "one moment, connecting you…" line spoken before the transfer.

dtmf_input
DtmfInputSpec · object

Per-node keypad-collection tuning (authoring config + runtime directive).

All fields optional: an unset field means "leave the assistant-level default in place" (the voice config's dtmf_* values). expects_input is the intent flag — a node that explicitly expects keypad entry — used so the host can apply the retune even when no numeric override differs from the default. prefix overrides the turn-text label (collection mechanics + framing).

session_status
string | null
error
string | null
model_used
string | null
token_count
integer | null
latency_ms
integer | null