# Conversations

> Multi-agent debates: put a question to all the agents of a workspace, let them argue for several rounds, and get a structured synthesis.

A **conversation** is a round table. You create one with a title, then submit a question: the agents of your workspace each give a first position, challenge each other over several rounds, and a synthesis comes back with the points of agreement, the risks, the conditions and a recommended action. It is the endpoint for decisions where one voice is not enough: a go/no-go, a pricing choice, a contract clause with commercial and legal readings.

For a dialogue with a single persona, use [Agent chat](https://dev.subsidia.protypa.fr/docs/agent-chat.md) instead. A debate is much heavier: it runs several model calls per agent per round.

**One call to POST /v1/conversations/:id/message runs the whole debate and returns once the synthesis is ready.**

Flow: Your question -> Initial positions (round 0) -> Debate rounds 1 to 3 -> Convergence check -> Synthesis

## How a debate runs

1. **Who sits at the table.** Up to **five** regular agents of the workspace (any agent not flagged `isDevilsAdvocate`) plus the workspace's **devil's advocate**, who is created automatically on first use and always joins. If the workspace has more than five regular agents, only the first five are seated. The workspace needs at least one regular agent, otherwise the call fails.
2. **Round 0.** Each agent answers the question in character, drawing on its memories and the documents linked to it.
3. **Debate rounds.** Up to **three** rounds in which each agent reads the others' positions and responds. After each round a convergence score is computed; at **0.85 or above** the debate stops early.
4. **Synthesis.** A final pass summarises where the table agrees, what was disputed, what could go wrong, and what to do. `totalRounds` is the number of debate rounds actually run.

Agents form memories during a debate like they do in chat ([Agent memory](https://dev.subsidia.protypa.fr/docs/agent-memory.md)), and the same conversation can receive several questions: each new message continues the same table with the earlier exchange as history.

> **WARNING: Synchronous and slow**
> The message endpoint holds the connection until the synthesis is ready. Expect tens of seconds with a cloud model and minutes on a small local machine. Set a client timeout of at least five minutes, do not retry on timeout (a retry runs a second debate and is billed again), and call it from a background job rather than from a user-facing request. The route is limited to 30 calls per minute.

> **INFO: Debates are billed as one workload**
> A debate consumes the workspace allowance according to the model usage of all its calls, which is far more than a chat turn. The call is refused before it starts if the allowance cannot cover a minimum debate. See [Rate limits and quotas](https://dev.subsidia.protypa.fr/docs/rate-limits.md).

## Authentication and scopes

Use a developer API key ([Authentication](https://dev.subsidia.protypa.fr/docs/authentication.md)). On a restricted key, the conversation routes need the `conversations` scope in addition to your capability scope. A `readonly` key can list and read conversations but cannot create, run or delete them. Conversations are scoped to the workspace of the key.

## Endpoints

### POST /v1/conversations

Create a conversation.

Creates an empty conversation (a table with no question yet). The title is shown in lists and is given to the agents as the topic of the debate, so write it as the question or the decision at stake, not as "Session 1".

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

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `title` | `string` | yes |  | Topic of the conversation. At least one character. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/conversations \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Should we open a second office in Lyon?"}'
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/conversations', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ title: 'Should we open a second office in Lyon?' }),
})
const { conversation } = await res.json()
console.log(conversation.id)
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/conversations",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={"title": "Should we open a second office in Lyon?"},
)
res.raise_for_status()
conversation_id = res.json()["conversation"]["id"]
```

#### Responses

**201**: The created conversation.

```json
{ "conversation": {
  "id": "cm2ke7p3b000gqz0fz5c1v8mn",
  "userId": "cm1u0a0000000qz0fowner001",
  "workspaceId": "cm1w0b0000000qz0fworksp01",
  "title": "Should we open a second office in Lyon?",
  "createdAt": "2026-10-09T10:02:44.318Z",
  "tokensUsed": 0,
  "shareToken": null,
  "isPublic": false,
  "goalOverride": null
} }
```

**400**: `title` is missing or empty.

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 |  | The body has no `title`, or it is empty. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |
| 403 | `scope_denied` | A restricted key without the `conversations` scope. |

### POST /v1/conversations/:id/message

Submit a question and run the debate.

Stores your message, runs the full debate (see [How a debate runs](#how-a-debate-runs)) and returns the synthesis. The individual positions are not in this response: they are saved as messages of the conversation, retrieve them with `GET /v1/conversations/:id`. There is no streaming variant on the public API.

Put everything the table needs in the message: the numbers, the options, the constraint. Agents read the documents linked to them, but a debate is only as good as its question.

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

#### Path parameters

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

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `content` | `string` | yes |  | The question or statement the agents debate. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/conversations/$CONVERSATION_ID/message \
  --max-time 600 \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "We have 3 letters of intent in Lyon and 5 months of cash buffer. Open an office now, wait, or hire remotely first?"}'
```

_TypeScript_

```typescript
const res = await fetch(
  'https://api.subsidia.protypa.fr/v1/conversations/' + conversationId + '/message',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      content: 'We have 3 letters of intent in Lyon and 5 months of cash buffer. Open an office now, wait, or hire remotely first?',
    }),
    signal: AbortSignal.timeout(10 * 60 * 1000),
  },
)
if (!res.ok) throw new Error(res.status + ' ' + (await res.text()))
const { synthesis, totalRounds } = await res.json()
console.log(totalRounds, 'rounds')
console.log(synthesis.recommendedAction)
console.log('Conditions:', synthesis.conditions)
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/conversations/" + conversation_id + "/message",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={"content": "We have 3 letters of intent in Lyon and 5 months of cash buffer. Open an office now, wait, or hire remotely first?"},
    timeout=600,
)
res.raise_for_status()
result = res.json()
print(result["totalRounds"], "rounds")
print(result["synthesis"]["recommendedAction"])
```

#### Responses

**200**: The synthesis, the number of rounds and the token accounting. `synthesis.agentInsights` has one entry per participant, the devil's advocate included; `shifted` is true when that agent changed position during the debate. `confidenceLevel` and `urgency` are short free-text labels (for example "low", "medium", "high"). `biasNotes` and `raw` (the unparsed synthesis text) may also be present.

```json
{
  "synthesis": {
    "summary": "The table favours opening in Lyon, but only after the second sales hire is confirmed.",
    "debateEvolution": "Claire moved from opposed to conditional after the cash-flow figures were put on the table.",
    "devilsAdvocateMainAttack": "A second office doubles fixed costs before the pipeline in Lyon is proven.",
    "agentInsights": [
      { "name": "Claire", "role": "Contrôle de gestion", "keyPoint": "Break-even needs 14 months at current margins.", "stance": "conditional", "shifted": true },
      { "name": "Marc", "role": "Directeur commercial", "keyPoint": "Three signed letters of intent in Lyon.", "stance": "for" }
    ],
    "consensus": ["The Lyon market is real.", "Do not sign a lease before the hire."],
    "riskPoints": ["Fixed-cost exposure", "Management attention split across two sites"],
    "recommendedAction": "Hire the Lyon sales lead first; revisit the lease in six months.",
    "conditions": ["Sales lead hired", "Cash reserve above three months"],
    "confidenceLevel": "medium",
    "urgency": "low",
    "roundCount": 2,
    "raw": "..."
  },
  "totalRounds": 2,
  "tokenUsage": { "totalTokensUsed": 41230, "remainingBalance": 958770 }
}
```

**400**: `content` is missing or empty.

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

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

**500**: The debate could not run. The message says why: no agents in the workspace (`No agents found. Please create agents first.`), an exhausted allowance, or a model failure.

```json
{ "error": "No agents found. Please create agents first." }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 |  | Empty or missing `content`. |
| 404 |  | Unknown conversation, or it belongs to another workspace. |
| 500 |  | No regular agent in the workspace, insufficient allowance, or model failure. Unlike chat, an exhausted allowance is not a 402 here: read the error message. |
| 429 | `rate_limited` | More than 30 calls per minute on this route, or the key's own limit. |

#### Notes

The first message of a conversation uses the conversation title as the topic when the title was set by you. Reuse a conversation to ask follow-ups on the same decision ("and if we delay the lease by six months?"): the table remembers the earlier exchange.

### GET /v1/conversations

List the conversations of the workspace.

Newest first, each with `_count.messages`. This list is the whole workspace table: it contains debates **and** the sessions created by [Agent chat](https://dev.subsidia.protypa.fr/docs/sessions.md), since both are conversations. It is not paginated.

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

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/conversations -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_Python_

```python
import os, requests

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

#### Responses

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

```json
{ "conversations": [
  {
  "id": "cm2ke7p3b000gqz0fz5c1v8mn",
  "userId": "cm1u0a0000000qz0fowner001",
  "workspaceId": "cm1w0b0000000qz0fworksp01",
  "title": "Should we open a second office in Lyon?",
  "createdAt": "2026-10-09T10:02:44.318Z",
  "tokensUsed": 0,
  "shareToken": null,
  "isPublic": false,
  "goalOverride": null,
  "_count": { "messages": 9 }
}
] }
```

### GET /v1/conversations/:id

Get a conversation with its messages and debate rounds.

Returns the full transcript: every message in chronological order and the debate rounds with their convergence scores. Message `senderType` is one of `user`, `agent`, `devils-advocate` and `synthesizer`. Agent messages carry `metadata.phase` (`initial` for round 0, `debate` for later rounds), `metadata.roundNumber` and `metadata.isDevilsAdvocate`; the `synthesizer` message holds the synthesis as `content` and its parsed fields in `metadata`.

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

#### Path parameters

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

#### Request examples

_curl_

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

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/conversations/' + conversationId, {
  headers: { Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY },
})
const { conversation } = await res.json()

for (const m of conversation.messages) {
  if (m.senderType === 'agent' || m.senderType === 'devils-advocate') {
    console.log('[round ' + m.metadata?.roundNumber + '] ' + m.agentName + ': ' + m.content)
  }
}
console.log(conversation.debates.map((d: any) => d.roundNumber + ': ' + d.convergenceScore))
```

#### Responses

**200**: The conversation with `messages` and `debates`.

```json
{
  "conversation": {
    "id": "cm2ke7p3b000gqz0fz5c1v8mn",
    "title": "Should we open a second office in Lyon?",
    "messages": [
      { "id": "cm2ke8a1c000hqz0f3k2m9d7x", "senderType": "user", "content": "We have 3 letters of intent in Lyon...", "createdAt": "2026-10-09T10:03:01.002Z" },
      {
        "id": "cm2ke8d4e000iqz0fp8w6n1ba",
        "senderType": "agent",
        "agentId": "cm2k8x1ab0001qz0f7h3d9t4e",
        "agentName": "Claire",
        "agentRole": "Contrôle de gestion",
        "content": "Break-even needs 14 months at current margins...",
        "metadata": { "phase": "initial", "roundNumber": 0, "isDevilsAdvocate": false },
        "createdAt": "2026-10-09T10:03:19.554Z"
      }
    ],
    "debates": [
      { "id": "cm2ke9z8f000jqz0fq4t1h6cy", "roundNumber": 1, "convergenceScore": 0.62, "createdAt": "2026-10-09T10:03:40.101Z" },
      { "id": "cm2kea5ng000kqz0fb7r3e9wk", "roundNumber": 2, "convergenceScore": 0.88, "createdAt": "2026-10-09T10:04:02.667Z" }
    ]
  }
}
```

**404**: Unknown conversation.

```json
{ "error": "Not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | No conversation with this id in the workspace. |

### DELETE /v1/conversations/:id

Delete a conversation and its messages.

Removes the conversation and its messages. It works for chat sessions. Memories the agents formed along the way are not removed.

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

#### Path parameters

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

#### Request examples

_curl_

```bash
curl -X DELETE https://api.subsidia.protypa.fr/v1/conversations/$CONVERSATION_ID \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

#### Responses

**204**: Deleted. No body.

**404**: Unknown conversation.

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

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | No conversation with this id in the workspace. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |

#### Notes

The route deletes the messages and the conversation row only. A conversation that has run a debate also has debate-round records attached, which this route does not remove, so deleting such a conversation can fail with a 500. Chat sessions have no such records.

## Working with debates

- **Seat the right people.** The cast is the first five regular agents of the workspace, in creation order. Keep a workspace for debates lean and deliberately diverse (finance, sales, legal, operations), and rely on the built-in devil's advocate rather than creating one.
- **Read the dissent, not just the summary.** `devilsAdvocateMainAttack`, `riskPoints` and `conditions` are where a debate earns its cost. A `consensus` reached in one round with a high convergence score on a weak question is a sign that your agents are too similar.
- **Store the transcript.** `GET /v1/conversations/:id` is the record of who said what in which round. Keep the conversation id next to the decision it informed.
- **A debate informs, a human decides.** The synthesis is a structured opinion, not a verdict.
