Conversation Endpoints
Manage agent conversation sessions. Served by the orchestration engine and
proxied through the API gateway at /api/v1/conversations.
All routes derive the requesting user from the gateway-injected
X-User-Id header (stamped from the R42 perimeter resolve, not raw claims).
The list is scoped to that user; PATCH/DELETE are owner-guarded (only the
owner — or a not-yet-attributed row — can mutate).
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/conversations | List the current user’s conversations |
POST | /api/v1/conversations | Create (or pre-register) a conversation |
GET | /api/v1/conversations/{id}/messages | Get conversation messages |
PATCH | /api/v1/conversations/{id} | Update title or status |
DELETE | /api/v1/conversations/{id} | Soft-delete a conversation |
POST | /api/v1/conversations/{id}/attachments | Upload a message attachment (multipart) |
GET | /api/v1/conversations/{id}/attachments | List the conversation’s attachments |
DELETE | /api/v1/conversations/{id}/attachments/{aid} | Delete an attachment |
GET /api/v1/conversations
List the authenticated user’s conversations, most recent first. Excludes
soft-deleted rows and harness runs (conversation_type of replay or
test). Returns 401 when no identity is present.
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/api/v1/conversations?limit=50"| Parameter | Type | Default | Description |
|---|---|---|---|
limit | int | 50 | Number of results (1–200) |
user_id | string | — | Fallback identity for local tooling; the X-User-Id header wins |
Response (200):
{
"conversations": [
{
"id": "conv-abc123",
"agent_id": "rule37-agent",
"user_id": "6ff89c0b-…",
"status": "active",
"title": "Spacing exception for Smith Ranch",
"preview": "Would you like me to proceed with the offset well analysis…",
"conversation_type": "filing_prep",
"last_message_at": "2026-07-07T14:35:00+00:00",
"created_at": "2026-07-07T14:30:00+00:00",
"updated_at": "2026-07-07T14:35:00+00:00",
"metadata": null
}
],
"total": 1
}title falls back to the first user message when no explicit title was set;
preview is the last message’s first 80 characters (for sidebar rows).
POST /api/v1/conversations
Create a conversation. Ownership comes from X-User-Id (body user_id is a
fallback for direct harness calls). The UI defers this call until the first
message is sent, so abandoned drafts never create rows.
Harnesses (golden capture, smoke runs) pre-register their conversation with
an explicit id and conversation_type: "test" so it never appears in a
user’s sidebar. The insert is idempotent on a caller-supplied id.
curl -X POST http://localhost:8000/api/v1/conversations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "rule37-agent",
"conversation_type": "filing_prep",
"title": "Spacing exception for Smith Ranch"
}'| Field | Type | Description |
|---|---|---|
agent_id | string | Required. Owning agent |
id | string | Optional caller-supplied id (harness pre-registration) |
title | string | Optional; defaults to "New conversation" |
conversation_type | string | filing_prep, field_event, or test |
user_id | string | Fallback owner when no X-User-Id header |
Response (200): {"id": "conv-new456…", "status": "created"}
GET /api/v1/conversations/{conversation_id}/messages
Retrieve all messages in a conversation, ordered by sequence_number.
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/v1/conversations/conv-abc123/messagesResponse (200):
{
"messages": [
{
"id": "…",
"conversation_id": "conv-abc123",
"sequence_number": 1,
"role": "user",
"content": "I need a spacing exception for well 42-383-12345",
"user_id": "6ff89c0b-…",
"tool_name": null,
"metadata": null,
"created_at": "2026-07-07T14:30:00+00:00"
}
],
"total": 1
}PATCH /api/v1/conversations/{conversation_id}
Update the title and/or status. Owner-guarded: with an X-User-Id present,
only the owner (or a not-yet-attributed row) can be updated; otherwise 404.
curl -X PATCH http://localhost:8000/api/v1/conversations/conv-abc123 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Updated title"}'DELETE /api/v1/conversations/{conversation_id}
Soft delete — sets deleted_at and preserves the data for the audit trail.
Owner-guarded like PATCH.
curl -X DELETE http://localhost:8000/api/v1/conversations/conv-abc123 \
-H "Authorization: Bearer $TOKEN"Response (200): {"id": "conv-abc123", "status": "deleted"}
POST /api/v1/conversations/{conversation_id}/attachments
Upload a file for use in a message (R40.5 P3). Multipart form with a single
file field. Extraction happens at upload time — the stored artifact is
the extracted TEXT (original bytes are not kept), so unreadable files fail
here with a human-readable detail the UI shows verbatim.
Limits: PDF / CSV / XLSX / DOCX / TXT, ≤10MB per file, ≤3 attachments per message, extracted text capped at ~8k tokens per attachment (tail-truncated, flagged in the response).
curl -X POST http://localhost:8000/api/v1/conversations/conv-abc123/attachments \
-H "Authorization: Bearer $TOKEN" \
-F "file=@spacing-report.pdf"Response (200):
{
"id": "0d0e…",
"filename": "spacing-report.pdf",
"content_type": "application/pdf",
"size_bytes": 48213,
"token_estimate": 2110,
"truncated": false
}Errors: 413 over 10MB · 415 disallowed type · 422 unreadable/empty
(encrypted PDF, image-only scan, blank file).
To use attachments in a turn, pass their ids on the send:
- SSE stream:
GET /conversations/{id}/stream?message=…&attachment_ids=<id1>,<id2> - Sync:
POST /executebody fieldattachment_ids: ["<id1>", …]
The server splices each file’s extracted text into the turn’s model context
(and the audit snapshot); the persisted user message keeps the typed text,
with pill refs {"attachments": [{"id", "filename"}]} in the user row’s
metadata. Ids are validated against the tenant and conversation — a
foreign id fails the request rather than being silently dropped.
Projects (R40.5 P4)
Owner-scoped containers under /api/v1/projects (identity from X-User-Id):
| Method | Path | Description |
|---|---|---|
GET | /api/v1/projects | List the caller’s projects (+ member counts) |
POST | /api/v1/projects | Create — {name, description?, instructions?, defaults?} |
GET | /api/v1/projects/{id} | Project + knowledge file list |
PATCH | /api/v1/projects/{id} | Update name/description/instructions/defaults |
DELETE | /api/v1/projects/{id} | Detaches member conversations (project_id → NULL), deletes knowledge |
POST | /api/v1/projects/{id}/knowledge | Upload a knowledge file (multipart; ≤5 per project, P3 extraction limits; 409 at the cap) |
DELETE | /api/v1/projects/{id}/knowledge/{fid} | Remove a knowledge file |
Conversations gain project_id: optional on POST /conversations (membership
is set at creation), returned on the list, and available as a ?project_id=
list filter. Turn behavior: each turn refreshes the project payload — its
defaults.thinking_mode joins the settings chain (request → project →
tenant → env, clamped by the tenant ceiling), instructions + knowledge inject
as system messages once per conversation (presence-scan deduped; the
injection ledger is marked per the spec contract).