---
title: "End a v2 conversation through the close pipeline (admin)"
method: POST
path: "/v1/v2/conversations/{conversation_id}/end_session"
tags: ["runtime v2"]
---

# End a v2 conversation through the close pipeline (admin)

`POST /v1/v2/conversations/{conversation_id}/end_session`

conv_* replacement for the retired ``POST /v1/runtime/end_session``.

The legacy endpoint drove ``Runtime.end_session(triggered_by="admin")``,
which under ``PersistStrategy.PER_TURN`` (every HTTP channel) reduced to the
same unified close pipeline ``SessionService.close_session`` now owns: flip
to CLOSED, stamp the meta close-out columns + retention, and enqueue the
post-session finalize chain (PII scrub → summarize + memory-sync). So this
is a straight call to ``close_session`` — there is no separate v2 ``Runtime``
to construct.

The conversation id is taken from the PATH and org-scoped (cross-org /
missing → 404 inside ``close_session`` → ``get_session_for_org``); the legacy
body's ``session_id`` / ``end_user_id`` / ``agent_id`` / ``channel`` fields
are accepted for wire-compat but ignored. ``reason`` defaults to
``AGENT_INITIATED`` (the same RESOLVED outcome category the legacy admin
close used when no explicit reason was supplied).

Response mirrors the legacy ``TurnResult`` shape (``messages=[]``,
``status="ended"``) — the admin UI ignores the body and only awaits the 200.

## Path parameters

- `conversation_id` string, uuid, required

## Request body

- EndSessionRequest — 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, nullable
  - `channel` string
  - `reason` string, nullable

## Response `200`

Successful Response

- TurnResult — 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 ``TurnResult``s 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.
  - `session_id` string, uuid, nullable
  - `messages` AssistantMessage[]
    - `turn_id` string, uuid, nullable
    - `content` string, required
    - `sequence` integer
    - `producing_assistant_id` string, uuid, nullable
  - `status` 'idle' | 'running_turn' | 'end_requested' | 'end_processing' | 'ended' | 'error', 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``.
  - `handoff` object, nullable
  - `call_transfer` CallTransferIntent — 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.
    - `destination` string, required
    - `mode` 'cold' | 'warm'
    - `label` string, nullable
    - `connecting_text` string, nullable
    - `reason` string, nullable
    - `warm_destinations` string[]
    - `max_retries` integer
    - `retry_text` string, nullable
    - `on_failure` string, nullable
    - `warm` WarmTransferConfig — Tuning for *warm* call transfer on the assistant ``transfer_call`` platform tool. Lives under ``platform_tools["transfer_call"]["warm"]`` alongside ``targets``. Warm is selected by authoring a target ``mode="warm"`` (its ``destination`` is the supervisor) — this block only carries the timeouts / message-mode knobs. Durations are bounded so a bad config can never strand a caller on hold indefinitely.
      - `message_mode` 'ai_summary' | 'static' | 'prompt'
      - `briefing_template` string, nullable
      - `max_dial_seconds` integer
      - `max_briefing_seconds` integer
      - `bridge_confirm_seconds` integer
      - `max_hold_seconds` integer
      - `hold_announcement_text` string
      - `reassurance_interval_seconds` integer
      - `reassurance_text` string
      - `detect_supervisor_voicemail` boolean
      - `detect_supervisor_silence` boolean
      - `silence_reminder_text` string
      - `silence_timeout_seconds` integer
      - `retry_announcement_text` string
      - `failure_text` string
    - `warm_overrides` WarmTransferTargetOverride — Per-destination override of the destination-specific subset of WarmTransferConfig. Lives on a ``TransferTargetConfig`` (assistant ``transfer_call`` tool target OR workflow ``HandoffNode.channels.voice`` target). ``None`` fields inherit the tool/node-level ``WarmTransferConfig`` baseline. Only the genuinely destination-specific fields are overridable here — the customer-experience + Twilio-mechanics timeouts (max_hold, reassurance, bridge_confirm, dial/briefing windows) stay tool/node-wide.
      - `briefing_template` string, nullable
      - `message_mode` 'ai_summary' | 'static' | 'prompt', nullable
  - `dtmf_input` DtmfInputSpec — 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).
    - `expects_input` boolean
    - `max_digits` integer, nullable
    - `termination_digit` string, nullable
    - `inter_digit_timeout_ms` integer, nullable
    - `prefix` string, nullable
  - `session_status` string, nullable
  - `error` string, nullable
  - `model_used` string, nullable
  - `token_count` integer, nullable
  - `latency_ms` integer, nullable

## Other responses

- `400` — Bad request
- `401` — Authentication required
- `404` — Resource not found
- `422` — Validation Error

---

[API](https://skmtc.net/oneloop-hq/apis/feather-api.md) · [All operations](https://skmtc.net/oneloop-hq/apis/feather-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/oneloop-hq/feather-api/versions/888bdd5c076e/schema)
