# Sessions

> Chat sessions keep the history of a conversation with an agent: create one, continue it, list and replay it.

A **session** is the thread of one conversation with an agent: the messages you sent and the answers you got, in order. Sending the same `sessionId` to [Agent chat](https://dev.subsidia.protypa.fr/docs/agent-chat.md) is what makes the second question understand the first. Without it, every call is a fresh conversation.

You rarely need to create a session explicitly. A chat call without a `sessionId` creates one and returns its id; keep that id and send it back. The endpoints on this page are for the cases where you want control: naming a session before the first message, listing the conversations a user had with an agent, or replaying a transcript.

## How history is kept

- **What is stored.** Each turn adds two messages to the session: yours (`senderType: "user"`) and the agent's (`senderType: "agent"`, with its `agentId`, `agentName` and `agentRole`). The agent message carries a `metadata` object with what happened during the turn: sources used, tool calls, consultations, memories saved, token usage, and the reasoning trace when requested.
- **What the agent sees.** For each new message the agent is shown the **20 most recent messages** of the session, not the whole thread. A conversation can grow without limit, but the agent's working context is a sliding window; put what must never be forgotten into [memory](https://dev.subsidia.protypa.fr/docs/agent-memory.md) or into the knowledge base.
- **Whose it is.** A session belongs to the user behind the API key that created it. Continuing it with a key of another user is a `404 Session not found`, even inside the same workspace. One application key therefore serves all your end users, and you map your users to session ids on your side.
- **Not tied to one agent.** Messages record who spoke, and the history shown to the model prefixes each agent answer with the agent's name. You can send the same `sessionId` to different agents (this is what auto-pick does implicitly) and each one sees the whole exchange.
- **Same table as conversations.** Sessions are workspace conversations: they appear in `GET /v1/conversations` and can be removed with `DELETE /v1/conversations/:id` ([Conversations](https://dev.subsidia.protypa.fr/docs/conversations.md)).

> **INFO: An empty session is invisible to the list**
> `GET /v1/agents/:id/sessions` and `GET /v1/agents/:id/sessions/:sid` only find sessions that contain at least one message **from that agent**. A session you just created and have not chatted in yet is a 404 on the read route until the agent has answered once.

## Typical flow

**Multi-turn conversation**

_TypeScript_

```typescript
const base = 'https://api.subsidia.protypa.fr/v1/agents/' + agentId
const headers = {
  Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
  'Content-Type': 'application/json',
}

async function ask(message: string, sessionId?: string) {
  const res = await fetch(base + '/chat', {
    method: 'POST',
    headers,
    body: JSON.stringify({ message, sessionId }),
  })
  if (!res.ok) throw new Error(res.status + ' ' + (await res.text()))
  return res.json()
}

const first = await ask('What is the Q3 invoice total?')
const second = await ask('And the previous quarter?', first.sessionId) // "previous" is understood

// Replay the whole transcript later.
const { session } = await (await fetch(base + '/sessions/' + first.sessionId, { headers })).json()
for (const m of session.messages) {
  console.log(m.senderType === 'user' ? 'You:' : m.agentName + ':', m.content)
}
```

_Python_

```python
import os, requests

headers = {"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]}
base = "https://api.subsidia.protypa.fr/v1/agents/" + agent_id


def ask(message, session_id=None):
    res = requests.post(base + "/chat", headers=headers,
                        json={"message": message, "sessionId": session_id}, timeout=300)
    res.raise_for_status()
    return res.json()


first = ask("What is the Q3 invoice total?")
second = ask("And the previous quarter?", first["sessionId"])  # "previous" is understood

session = requests.get(base + "/sessions/" + first["sessionId"], headers=headers).json()["session"]
for m in session["messages"]:
    speaker = "You" if m["senderType"] == "user" else m["agentName"]
    print(speaker + ":", m["content"])
```

## Endpoints

All three routes need the `agents` scope on restricted keys and are workspace-scoped: another workspace's session is indistinguishable from a missing one. Creating a session is refused for `readonly` keys.

### POST /v1/agents/:id/sessions

Create an empty session for an agent.

Creates a session owned by the user of the key and returns it. The agent must exist in the workspace, but the session is not locked to it. Pass the returned `id` as `sessionId` in the chat calls. Nothing is billed.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `title` | `string` | no | `"Chat with <agent name>"` | A label for the session. Without it, the title is "Chat with" followed by the agent name. (Sessions created implicitly by a chat call are titled with the first 80 characters of the first message.) |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/sessions \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Moreau - Q3 review"}'
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/agents/' + agentId + '/sessions', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'Moreau - Q3 review' }),
})
const { session } = await res.json()
console.log(session.id)
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id + "/sessions",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={"title": "Moreau - Q3 review"},
)
res.raise_for_status()
session_id = res.json()["session"]["id"]
```

#### Responses

**201**: The new session.

```json
{ "session": {
  "id": "cm2kc4t9p000bqz0fr6s2a8dk",
  "userId": "cm1u0a0000000qz0fowner001",
  "workspaceId": "cm1w0b0000000qz0fworksp01",
  "title": "Moreau - Q3 review",
  "createdAt": "2026-10-09T09:12:30.004Z",
  "tokensUsed": 0,
  "shareToken": null,
  "isPublic": false,
  "goalOverride": null
} }
```

**404**: No agent with this id in the workspace.

```json
{ "error": "Agent not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | Unknown agent id. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |

### GET /v1/agents/:id/sessions

List the sessions in which an agent has spoken.

Returns the workspace conversations that contain at least one message from this agent, newest first, each with a `_count.messages`. The list covers the whole workspace, not only your key's sessions. It is not paginated and does not include the messages themselves: fetch a single session for those.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/sessions \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_Python_

```python
import os, requests

res = requests.get(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id + "/sessions",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
)
for s in res.json()["sessions"]:
    print(s["id"], s["title"], s["_count"]["messages"], "messages")
```

#### Responses

**200**: An object with a `sessions` array.

```json
{ "sessions": [
  {
  "id": "cm2kc4t9p000bqz0fr6s2a8dk",
  "userId": "cm1u0a0000000qz0fowner001",
  "workspaceId": "cm1w0b0000000qz0fworksp01",
  "title": "Chat with Claire",
  "createdAt": "2026-10-09T09:12:30.004Z",
  "tokensUsed": 0,
  "shareToken": null,
  "isPublic": false,
  "goalOverride": null,
  "_count": { "messages": 4 }
}
] }
```

**404**: No agent with this id in the workspace.

```json
{ "error": "Agent not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | Unknown agent id. |

### GET /v1/agents/:id/sessions/:sid

Get a session with its full message history.

Returns the session and all its messages in chronological order, with the metadata of each agent message (sources, tool calls, tokens). The session must belong to the workspace and contain a message from the agent in the path; otherwise it is a 404.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |
| `sid` | `string` | yes |  | The session id. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/sessions/$SESSION_ID \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_TypeScript_

```typescript
const res = await fetch(
  'https://api.subsidia.protypa.fr/v1/agents/' + agentId + '/sessions/' + sessionId,
  { headers: { Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY } },
)
if (res.status === 404) throw new Error('Unknown session, or the agent has not answered in it yet')
const { session } = await res.json()
console.log(session.messages.length, 'messages')
```

#### Responses

**200**: The session with a `messages` array. User messages have no metadata; agent messages carry `thoughts`, `memoriesSaved`, `tokenUsage`, `knowledgeUsed`, `contextUsed`, `toolCalls`, `consultations` and `masked` when they apply.

```json
{
  "session": {
    "id": "cm2kc4t9p000bqz0fr6s2a8dk",
    "title": "What is the Q3 invoice total?",
    "createdAt": "2026-10-09T09:12:30.004Z",
    "messages": [
      {
        "id": "cm2kc4u1x000cqz0f2m9b7e3t",
        "conversationId": "cm2kc4t9p000bqz0fr6s2a8dk",
        "userId": null,
        "senderType": "user",
        "agentId": null,
        "agentName": null,
        "agentRole": null,
        "content": "What is the Q3 invoice total?",
        "metadata": null,
        "createdAt": "2026-10-09T09:12:30.210Z"
      },
      {
        "id": "cm2kc4y5a000dqz0fk1d4n8vw",
        "conversationId": "cm2kc4t9p000bqz0fr6s2a8dk",
        "userId": null,
        "senderType": "agent",
        "agentId": "cm2k8x1ab0001qz0f7h3d9t4e",
        "agentName": "Claire",
        "agentRole": "Contrôle de gestion",
        "content": "The Q3 invoice is 12 480 EUR excluding VAT [1].",
        "metadata": { "tokenUsage": { "promptTokens": 2140, "completionTokens": 14, "totalTokens": 2154 }, "toolCalls": null },
        "createdAt": "2026-10-09T09:12:36.774Z"
      }
    ]
  }
}
```

**404**: Unknown session, a session of another workspace, or a session in which this agent never spoke.

```json
{ "error": "Session not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | Unknown session id, wrong workspace, or no message from this agent in it. |

## Deleting a session

There is no session-specific delete route. A session is a workspace conversation: delete it with [`DELETE /v1/conversations/:id`](https://dev.subsidia.protypa.fr/docs/conversations.md), which removes the thread and all its messages. Memories the agent wrote during the session are not removed; manage them with [Agent memory](https://dev.subsidia.protypa.fr/docs/agent-memory.md).
