When a workflow has Session memory enabled, every turn sent with a session_id is stored in ModelRiver (see Session memory). The Sessions API lets your backend read those conversations and add turns to them, so you don't need to keep your own copy of the chat history.
Typical uses:
- Show a user's past conversations in your app.
- Hand a chat over to a human agent: store the user's messages and the agent's replies in the same session, so the AI sees what the agent said when it takes over again.
- Show new turns the moment they happen, in your app or your agents' dashboard, with live updates.
- Reset a conversation by deleting its session.
All endpoints use the same project API key as /v1/ai (Authorization: Bearer mr_live_…) and only see sessions in that key's project. A session from another project, or an id that isn't a UUID, returns 404 {"error": "Session not found"}.
List sessions
1GET /v1/sessions?workflow=support_bot&limit=20&page=1| Parameter | Default | Notes |
|---|---|---|
workflow | all | Only sessions of the workflow with this name |
limit | 20 | Page size, at most 100 |
page | 1 | 1-based page number |
Sessions are sorted by most recent activity first.
1{2 "data": [3 {4 "id": "9f8c2c1e-5b8a-4d0e-9a51-2f6f0f1d7a10",5 "workflow": "support_bot",6 "message_count": 6,7 "last_activity_at": "2026-10-25T09:14:03.512Z",8 "inserted_at": "2026-10-25T08:58:41.007Z",9 "last_message": {10 "role": "assistant",11 "content": "Your refund was issued today.",12 "at": "2026-10-25T09:14:03.498Z"13 }14 }15 ],16 "page": 1,17 "limit": 2018}message_count counts stored turns: each AI reply and each appended message counts as one. last_message is null when the latest turn has no readable text.
Read a session
1GET /v1/sessions/:id1{2 "id": "9f8c2c1e-5b8a-4d0e-9a51-2f6f0f1d7a10",3 "workflow": "support_bot",4 "inserted_at": "2026-10-25T08:58:41.007Z",5 "last_activity_at": "2026-10-25T09:14:03.512Z",6 "messages": [7 {8 "id": "4b1d…:user",9 "role": "user",10 "author": "user",11 "author_name": null,12 "content": "I was charged twice for order 1234",13 "created_at": "2026-10-25T08:58:40.112Z"14 },15 {16 "id": "4b1d…:assistant",17 "role": "assistant",18 "author": "ai",19 "author_name": null,20 "content": "Sorry about that. Let me connect you with our billing team.",21 "created_at": "2026-10-25T08:58:41.007Z"22 },23 {24 "id": "7e02…:assistant",25 "role": "assistant",26 "author": "agent",27 "author_name": "Dana",28 "content": "Hi, I'm Dana. I've refunded the duplicate charge.",29 "created_at": "2026-10-25T09:14:03.498Z"30 }31 ],32 "truncated": false33}Messages are sorted oldest first.
| Field | Description |
|---|---|
id | Stable message id. Treat it as an opaque string. |
role | user or assistant |
author | user, ai (a model reply) or agent (a human reply appended with this API) |
author_name | The author_name sent when the message was appended, otherwise null |
created_at | When the turn was stored |
truncated | true when the session has more than the 100 most recent turns returned here |
Only the conversation is returned. System prompts, injected session memory and ModelRiver's built-in safety instructions are left out, and so are replies from failed requests. Turns are kept for your plan's log retention period.
Append a message
Stores a turn in the session without calling a model. Nothing is billed and it doesn't count as a request.
1POST /v1/sessions/:id/messages2Content-Type: application/json3 4{ "role": "assistant", "content": "Hi, I'm Dana. I've refunded the duplicate charge.", "author_name": "Dana" }| Field | Required | Notes |
|---|---|---|
role | ✅ | user stores a user message; assistant stores a human agent's reply (author: "agent") |
content | ✅ | Non-empty text, up to 100,000 characters |
author_name | ❌ | Display name, up to 200 characters |
Response 201:
1{2 "id": "7e02…:assistant",3 "role": "assistant",4 "author": "agent",5 "author_name": "Dana",6 "content": "Hi, I'm Dana. I've refunded the duplicate charge.",7 "created_at": "2026-10-25T09:14:03.498Z"8}Appended turns become part of session memory. The next /v1/ai request with this session_id sees the user messages and agent replies as earlier conversation, so the AI can continue where the agent left off.
A missing or invalid field returns 400 with an error message. Appending also returns 400 when the project has stopped storing request bodies, because session content can't be saved.
Live updates
Follow a session as it happens instead of polling: new turns, events your app sends, and deletion arrive over the same WebSocket that streams async responses.
-
Your backend asks for a one-time token for the session:
HTTP1POST /v1/sessions/:id/subscribeJSON1{2 "session_id": "4b1d…",3 "ws_token": "Qm9i…",4 "websocket_url": "wss://api.modelriver.com/socket",5 "websocket_channel": "session:7c2e…:4b1d…",6 "expires_in": 3007} -
Your frontend joins
websocket_channelwithws_token. The client SDK does this, and asks your backend for a new token whenever it reconnects:TYPESCRIPT1import { subscribeToSession } from '@modelriver/client';23const subscription = subscribeToSession({4 // Your backend route that calls POST /v1/sessions/:id/subscribe5 getSubscription: () => fetch(`/api/chat/${chatId}/live`, { method: 'POST' }).then((r) => r.json()),6 onMessage: (message) => addToChat(message),7 onSignal: (signal) => { if (signal.name === 'agent.joined') showAgent(signal.data); },8 onDeleted: () => closeChat(),9});1011// Later12subscription.close();
A token works once, only for that session, and expires after five minutes if unused. Connections close after an hour; the SDK reconnects with a fresh token. Ask for tokens only for users who may see the conversation, since the channel carries its messages.
Events
| Event | Payload | When |
|---|---|---|
message | A message as in Read a session, plus session_id | A /v1/ai request in the session succeeds (its user message, then the reply), or a turn is appended |
signal | { "session_id", "name", "data", "sent_at" } | Your backend sends a signal |
deleted | { "session_id" } | The session is deleted |
Failed requests and test-mode runs are not announced. Message ids match the ones GET /v1/sessions/:id returns, so a client can merge live messages into a history it loaded earlier without duplicates. Events sent while a client is disconnected are not replayed: after reconnecting, read the session again to catch up (the SDK's onLive callback tells you when).
Send a signal
Signals tell subscribers about things that aren't conversation turns, such as "an agent joined", "the chat was closed" or "someone is typing". They are delivered to whoever is connected and never stored, so they don't appear in the transcript or in session memory.
1POST /v1/sessions/:id/signals2Content-Type: application/json3 4{ "name": "agent.joined", "data": { "name": "Dana" } }| Field | Required | Notes |
|---|---|---|
name | ✅ | 1–64 characters: lowercase letters, digits, _ . : - |
data | ❌ | Any JSON, up to 4 KB |
Response 202: { "status": "sent" }.
Delete a session
1DELETE /v1/sessions/:id1{ "message": "success", "status": "deleted" }Deleting a session removes its id and memory and sends deleted to its live subscribers, the same as deleting it in the console under Logs → Sessions. Request logs stay for audit purposes. Send the next /v1/ai request without session_id to start a new conversation. On a protected project, deleting returns 403 project_protected; delete the session from the console as an organization owner or admin instead.
Next steps
- Endpoints: Send AI requests with
session_id - Authentication: Create and rotate API keys
- Error handling: Status codes and retries