> ## Documentation Index
> Fetch the complete documentation index at: https://doc.featherhq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Run Multi-Turn Conversations with a Feather Agent

> Open a conversation session, send user messages, stream agent responses, and close the session when done — with full code examples.

The Feather conversation API is built around **sessions** — stateful containers that track the full message history, context variables, and session lifecycle for a single end-user interaction. Within a session you can send synchronous turns, stream tokens over SSE for real-time UIs, poll for operator replies during a human handoff, and retrieve a paginated transcript at any time. This guide walks through each operation with working curl examples.

***

## Create a session

Open a new conversation session by associating an end user, a channel, and the agent you want to run.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api-sandbox.featherhq.com/v1/v2/conversations \
    -H "x-api-key: $FEATHER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "end_user_id": "550e8400-e29b-41d4-a716-446655440000",
      "channel": "api",
      "assistant_id": "agt_01hx3k2mz9vbqwerty123456",
      "session_type": "live"
    }'
  ```
</CodeGroup>

```json theme={null}
{
  "id": "conv_01hxabc987654321fedcba00",
  "end_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "channel": "api",
  "assistant_id": "agt_01hx3k2mz9vbqwerty123456",
  "status": "active",
  "session_type": "live",
  "created_at": "2024-05-14T12:00:00Z"
}
```

| Field          | Values                       | Description                                                                                                                                          |
| -------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel`      | `api`, `web`, `email`, `sms` | The delivery channel for this conversation                                                                                                           |
| `session_type` | `live`, `test`, `simulation` | `live` sessions are persisted and trigger post-session processing; `test` sessions are sandboxed; `simulation` sessions back automated scenario runs |
| `status`       | `active`, `closed`           | Lifecycle state of the conversation                                                                                                                  |

<Tip>
  Pass a `context_variables` object in the request body to seed the conversation with pre-known data — for example, `{"customer_name": "Alex", "account_tier": "premium"}`. These values are injected into the agent's system prompt and tool calls automatically.
</Tip>

***

## Send turns (synchronous)

For standard request/response flows, post a turn and wait for the full agent reply.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api-sandbox.featherhq.com/v1/v2/conversations/conv_01hxabc987654321fedcba00/turns \
    -H "x-api-key: $FEATHER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "user_message": "I need help with my order #12345"
    }'
  ```
</CodeGroup>

```json theme={null}
{
  "turn_id": "turn_01hxbcd111222333444aaaa",
  "conversation_id": "conv_01hxabc987654321fedcba00",
  "text": "I'd be happy to help with order #12345! Let me look that up for you right now.",
  "session_status": "active",
  "message_seqs": [2, 3]
}
```

| Field             | Description                                                                            |
| ----------------- | -------------------------------------------------------------------------------------- |
| `turn_id`         | Unique identifier for this turn                                                        |
| `conversation_id` | The conversation this turn belongs to                                                  |
| `text`            | The agent's final reply text                                                           |
| `session_status`  | Current conversation status (`active`, `waiting_for_human`, `closed`)                  |
| `message_seqs`    | Sequence numbers of the messages written in this turn — use as a `since_seq` watermark |

<Note>
  If `session_status` changes to `waiting_for_human`, the agent has triggered a human handoff. No further AI replies will be generated until an operator responds or the session is returned to the bot.
</Note>

***

## Stream turns (SSE)

For chat UIs where you want to render tokens as they arrive, use the streaming endpoint. It returns a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream with the same request body as the synchronous turn endpoint.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api-sandbox.featherhq.com/v1/v2/conversations/conv_01hxabc987654321fedcba00/turns/stream \
    -H "x-api-key: $FEATHER_API_KEY" \
    -H "Content-Type: application/json" \
    --no-buffer \
    -d '{
      "user_message": "What is the status of my order?"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    'https://api-sandbox.featherhq.com/v1/v2/conversations/conv_01hxabc987654321fedcba00/turns/stream',
    {
      method: 'POST',
      headers: {
        'x-api-key': `${process.env.FEATHER_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ user_message: 'What is the status of my order?' }),
    }
  );

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    // SSE frames are separated by a blank line; each frame has an
    // `event:` line and a `data:` line.
    let sep;
    while ((sep = buffer.indexOf('\n\n')) !== -1) {
      const frame = buffer.slice(0, sep);
      buffer = buffer.slice(sep + 2);

      let eventName = 'message';
      let data = '';
      for (const line of frame.split('\n')) {
        if (line.startsWith('event:')) eventName = line.slice(6).trim();
        else if (line.startsWith('data:')) data += line.slice(5).trim();
      }
      if (!data) continue;
      const payload = JSON.parse(data);

      if (eventName === 'delta') {
        process.stdout.write(payload.text); // stream tokens to UI
      } else if (eventName === 'complete') {
        console.log('\n\nFull turn:', payload); // final turn object (inline fields)
      } else if (eventName === 'error') {
        console.error('Stream error:', payload.error);
      }
    }
  }
  ```
</CodeGroup>

**Example SSE stream:**

```
event: delta
data: {"text":"Your order "}

event: delta
data: {"text":"#12345 was "}

event: delta
data: {"text":"shipped on May 13th "}

event: delta
data: {"text":"and is expected to arrive by May 16th."}

event: complete
data: {"turn_id":"turn_01hxbcd111222333444bbbb","conversation_id":"conv_01hxabc987654321fedcba00","text":"Your order #12345 was shipped on May 13th and is expected to arrive by May 16th.","active_assistant_id":"agt_01hx3k2mz9vbqwerty123456","path":"single","decision_source":"router","message_seqs":[4,5],"session_status":"active","turn_ttft_ms":312,"perceived_ttft_ms":340,"ended":false}
```

**Event types:**

Each frame carries a named SSE `event:` line followed by a `data:` line — branch on the event name, not on any field inside the payload.

| SSE `event:` | Data payload                                                                                                                                                 |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `delta`      | `{"text": "..."}` — an incremental text token to append to the UI                                                                                            |
| `complete`   | The final turn object with its fields inline (`turn_id`, `conversation_id`, `text`, `session_status`, `message_seqs`, …) — treat this as the source of truth |
| `error`      | `{"error": "..."}` — a fatal error occurred; the stream closes immediately after this event                                                                  |

<Warning>
  Always handle the `error` event explicitly. If the stream closes without a `complete` event, assume the turn failed and surface an appropriate message to the user.
</Warning>

***

## Poll messages (for async channels)

When your agent is connected to an asynchronous channel (email, SMS, or a human handoff queue) you need to poll for new messages rather than waiting on an open HTTP connection.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api-sandbox.featherhq.com/v1/v2/conversations/conv_01hxabc987654321fedcba00/messages?since_seq=-1" \
    -H "x-api-key: $FEATHER_API_KEY"
  ```
</CodeGroup>

```json theme={null}
{
  "session_status": "waiting_for_human",
  "next_seq": 7,
  "messages": [
    {
      "id": "msg_01hxbcd555666777888aaaa",
      "role": "assistant",
      "content": "I'm connecting you with a support specialist now. Please hold on.",
      "authored_by": "agent",
      "created_at": "2024-05-14T12:05:00Z"
    },
    {
      "id": "msg_01hxbcd555666777888bbbb",
      "role": "assistant",
      "content": "Hi, this is Jamie from support. How can I help you today?",
      "authored_by": "human_operator",
      "created_at": "2024-05-14T12:06:45Z"
    }
  ]
}
```

Both AI and operator messages use `role: "assistant"`; distinguish them by `authored_by` — `"agent"` for AI-authored replies and `"human_operator"` for operator relays. The `role` enum is `user`, `assistant`, `system`, `tool`, or `system_event`.

Pass `since_seq=-1` on the first request to retrieve all messages from the beginning. On subsequent polls, pass the `next_seq` value from the previous response to receive only new messages.

<Tip>
  For human handoff workflows, poll every 2–5 seconds while `session_status` is `waiting_for_human`. Once it returns to `active`, the operator has handed control back to the agent and you can resume normal turn-based messaging.
</Tip>

***

## Close a session

Signal that the conversation is resolved by closing the session. This triggers any configured post-session webhooks, saves the conversation to long-term memory, and makes the session eligible for analytics processing.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api-sandbox.featherhq.com/v1/v2/conversations/conv_01hxabc987654321fedcba00/close \
    -H "x-api-key: $FEATHER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "reason": "resolved"
    }'
  ```
</CodeGroup>

```json theme={null}
{
  "id": "conv_01hxabc987654321fedcba00",
  "status": "closed",
  "closed_at": "2024-05-14T12:15:00Z"
}
```

<Note>
  A session **must be closed** before it can be deleted. Attempting to delete an active session will return a `409 Conflict` error.
</Note>

***

## Retrieve transcript

Fetch a paginated list of all turns in a conversation — useful for post-session review, debugging, or exporting conversation logs.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api-sandbox.featherhq.com/v1/v2/conversations/conv_01hxabc987654321fedcba00/turns?limit=20" \
    -H "x-api-key: $FEATHER_API_KEY"
  ```
</CodeGroup>

Each element of `turns` is a single message row — one per message, carrying a `role` and `content` rather than a paired user/assistant turn.

```json theme={null}
{
  "turns": [
    {
      "id": "msg_01hxbcd111222333444aaaa",
      "session_id": "conv_01hxabc987654321fedcba00",
      "role": "user",
      "content": "I need help with my order #12345",
      "created_at": "2024-05-14T12:01:00Z"
    },
    {
      "id": "msg_01hxbcd111222333444bbbb",
      "session_id": "conv_01hxabc987654321fedcba00",
      "role": "assistant",
      "content": "I'd be happy to help with order #12345! Let me look that up for you right now.",
      "created_at": "2024-05-14T12:01:02Z"
    }
  ],
  "next_cursor": "dHVybl8wMWh4YmNkMTExMjIyMzMzNDQ0YmJiYg==",
  "has_more": false
}
```

Paginate by passing `cursor=<next_cursor>` and `limit=<n>` as query parameters. When `has_more` is `false`, you've reached the end of the transcript.

***

## What's next?

<CardGroup cols={2}>
  <Card title="Conversations Concept Guide" icon="comment-dots" href="/concepts/conversations">
    Deep dive into session lifecycle, message sequencing, and context variable propagation.
  </Card>

  <Card title="Human-in-the-Loop" icon="user-headset" href="/guides/human-in-the-loop">
    Configure escalation rules, operator queues, and handoff triggers for live agent support.
  </Card>
</CardGroup>
