Interactive Sessions: API Reference
Complete REST API documentation for interactive sessions.
Base URL: https://agent-session-api.xema.dev
Authentication: Bearer token required on all endpoints.
Sessions
Create Session
POST /sessions
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
agentRef | string | Conditional | Published Agent slug or slug@version; omit only when a registered sessionDomainKey resolves it |
sessionDomainKey | string | Conditional | Domain projection kind; requires sessionDomainRef |
sessionDomainRef | string | Conditional | Opaque domain resource id; requires sessionDomainKey |
title | string | No | Human-readable session title |
modelId | string | No | Override the default model |
repositoryId | string | No | SCM repository to link |
branchStrategy | string | No | auto_create | use_existing | none |
budgetLimit | number | No | Max token budget |
customConfig | object | No | Advanced agent config overrides |
lifecyclePolicy | string | No | ephemeral (default; pauses when idle) or persistent (never pauses for idleness) |
placement | string | No | reuse_if_available or force_isolated |
Example:
1curl -X POST https://agent-session-api.xema.dev/sessions \
2 -H "Authorization: Bearer $TOKEN" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "agentRef": "engineering-assistant@3",
6 "title": "Refactor auth module",
7 "repositoryId": "repo-123",
8 "branchStrategy": "auto_create",
9 "budgetLimit": 200000
10 }'Response (201):
1{
2 "id": "clxxxxxxxxxxxxxxxxxx",
3 "orgId": "org-123",
4 "projectId": "proj-456",
5 "title": "Refactor auth module",
6 "status": "creating",
7 "agentSlug": "engineering",
8 "modelId": "anthropic/claude-sonnet-4-20250514",
9 "tokenUsage": null,
10 "budgetLimit": 200000,
11 "branchName": null,
12 "branchStrategy": "auto_create",
13 "repositoryId": "repo-123",
14 "pipelineRunId": null,
15 "parentSessionId": null,
16 "prUrl": null,
17 "failureReason": null,
18 "provisioningPhase": "queued",
19 "createdAt": "2026-04-27T10:30:00Z",
20 "lastActivityAt": "2026-04-27T10:30:00Z"
21}Get Session
GET /sessions/{id}
Response (200): Session object (same structure as create response).
List Sessions
GET /sessions
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status |
pipelineRunId | string | Filter by pipeline run |
page | number | Page number (default 1) |
limit | number | Max results per page (default 20) |
Example:
1curl "https://agent-session-api.xema.dev/sessions?status=active" \
2 -H "Authorization: Bearer $TOKEN"Pause Session
POST /sessions/{id}/pause
Session must be active with every thread idle (no turn in flight). Saves the workspace snapshot and releases the worker.
Response (200):
1{
2 "paused": true
3}Resume Session
POST /sessions/{id}/resume
Session must be paused. The row transitions to recovering, acquires a fresh worker, restores the saved state, then returns to active.
Response (200):
1{
2 "resumed": true
3}Fork Session
POST /sessions/{id}/fork
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Title for the forked session |
Response (201): New session object with parentSessionId set.
Threads
A session hosts one or more threads — independent conversation streams that share the session's single worker and workspace. Every session is created with one default thread (Main); open more to run parallel conversations against the same files. Each thread has its own turn lock (turnState) and its own message history.
List Threads
GET /sessions/{id}/threads
Response (200):
1{
2 "data": [
3 {
4 "id": "thr_main",
5 "sessionId": "cl123",
6 "title": "Main",
7 "status": "active",
8 "turnState": "idle",
9 "agentSlug": null,
10 "createdAt": "2026-04-27T10:30:00Z",
11 "lastActivityAt": "2026-04-27T10:34:12Z"
12 }
13 ]
14}Create Thread
POST /sessions/{id}/threads
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Human-readable thread title (defaults to New thread) |
agentSlug | string | No | Per-thread agent override (defaults to the session's agent) |
Response (201): Thread object.
Update Thread
PATCH /sessions/{id}/threads/{threadId}
Rename a thread or change its lifecycle status.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | New title |
status | string | No | active | archived |
A session must always retain at least one active thread — archiving the last active thread is rejected with 409.
Delete Thread
DELETE /sessions/{id}/threads/{threadId}
Permanently deletes a thread and its messages. The session's last active thread cannot be deleted (409).
Messages
Send Message
POST /sessions/{id}/threads/{threadId}/messages
Returns a streaming SSE response (text/event-stream). The message is sent to a specific thread — use the session's default thread when you are not managing multiple conversations.
Headers:
| Header | Description |
|---|---|
Idempotency-Key | (Recommended) Unique key to prevent duplicate sends |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Message content (max 100KB) |
Example:
1curl -X POST \
2 https://agent-session-api.xema.dev/sessions/cl123/threads/thr_main/messages \
3 -H "Authorization: Bearer $TOKEN" \
4 -H "Content-Type: application/json" \
5 -H "Accept: text/event-stream" \
6 -H "Idempotency-Key: $(uuidgen)" \
7 -d '{"content": "Review the authentication module and suggest improvements."}'SSE Event Types:
Every data: payload carries the threadId it belongs to, so a client multiplexing several threads over one connection can route each frame.
1event: text_delta
2data: {"delta": "Looking at the authentication module...", "sessionId": "cl123", "threadId": "thr_main"}
3
4event: thinking
5data: {"delta": "The user wants a code review..."}
6
7event: tool_call
8data: {"toolName": "read_file", "args": {"path": "src/auth/jwt.service.ts"}, "status": "started"}
9
10event: file_change
11data: {"operation": "edited", "path": "src/auth/jwt.service.ts", "diff": "..."}
12
13event: todo_updated
14data: {"todos": [{"id": "1", "content": "Review JWT logic", "done": true}, ...]}
15
16event: question_asked
17data: {"questionId": "q_abc", "question": "Should I use RS256 or HS256?"}
18
19event: permission_asked
20data: {"permissionId": "p_abc", "action": "delete", "resource": "src/legacy/old-auth.ts"}
21
22event: done
23data: {"messageId": "msg_xyz", "totalTokens": 8500}
24
25event: error
26data: {"code": "TOOL_FAILED", "message": "Could not read file"}List Messages
GET /sessions/{id}/threads/{threadId}/messages
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | number | Max messages (default 50) |
cursor | string | Pagination cursor |
Response (200):
1{
2 "data": [
3 {
4 "id": "msg_abc",
5 "sessionId": "cl123",
6 "threadId": "thr_main",
7 "role": "user",
8 "content": "Review the auth module",
9 "createdAt": "2026-04-27T10:30:00Z"
10 },
11 {
12 "id": "msg_def",
13 "sessionId": "cl123",
14 "threadId": "thr_main",
15 "role": "assistant",
16 "content": "I've reviewed the JWT authentication...",
17 "metadata": {
18 "tools": ["read_file", "list_directory"],
19 "inputTokens": 3200,
20 "outputTokens": 1800
21 },
22 "createdAt": "2026-04-27T10:30:45Z"
23 }
24 ],
25 "nextCursor": null
26}Questions & Permissions
Each question and permission ask is a decision, and the first answer wins. In an unattended session they are denied at once (see Attendance).
Answer a Question
POST /sessions/{id}/questions/{questionId}/answer
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
answer | string | Yes | Your answer to the agent's question |
Reject a Question
POST /sessions/{id}/questions/{questionId}/reject
The agent will proceed without an answer.
Reply to Permission Request
POST /sessions/{id}/permissions/{permissionId}/reply
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
granted | boolean | Yes | Whether to grant the permission |
List and Revoke Always-Allow Grants
GET /sessions/{id}/permission-grants
Always-allow grants are scoped to the session. List them here and revoke one at the same path.
Events
List Events
GET /sessions/{id}/events
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
type | string | Filter by event type |
afterSeq | number | Get events after sequence number |
limit | number | Max results |
Response (200):
1{
2 "data": [
3 {
4 "id": "evt_abc",
5 "sessionId": "cl123",
6 "threadId": "thr_main",
7 "seq": 1,
8 "type": "status_changed",
9 "payload": {"from": "creating", "to": "active"},
10 "createdAt": "2026-04-27T10:30:01Z"
11 },
12 {
13 "id": "evt_def",
14 "sessionId": "cl123",
15 "threadId": "thr_main",
16 "seq": 2,
17 "type": "file_created",
18 "payload": {"path": "src/auth/token-rotation.service.ts"},
19 "createdAt": "2026-04-27T10:31:00Z"
20 }
21 ]
22}Git Operations
Get Git Status
GET /sessions/{id}/git/status
Response (200):
1{
2 "branch": "interactive/fix-auth-abc123",
3 "status": "clean",
4 "changes": [],
5 "commits": [
6 {"hash": "a1b2c3d", "message": "Add JWT token rotation", "author": "agent"}
7 ],
8 "pullRequest": {
9 "url": "https://github.com/org/repo/pull/42",
10 "number": 42,
11 "title": "Refactor JWT authentication"
12 }
13}Create Branch
POST /sessions/{id}/git/branch
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
branchName | string | Yes | Branch name to create |
Commit Changes
POST /sessions/{id}/git/commit
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Commit message |
files | string[] | No | Specific files to commit (default: all staged) |
Create Pull Request
POST /sessions/{id}/git/pull-requests
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | PR title |
description | string | No | PR description |
baseBranch | string | No | Target branch (default: main) |
Check Merge Status
GET /sessions/{id}/git/merge-check?baseBranch=main
Response (200):
1{
2 "canMerge": true,
3 "conflicts": [],
4 "checks": [
5 {"name": "no-conflicts", "passed": true},
6 {"name": "up-to-date", "passed": true}
7 ]
8}MCP Tool Selection
A session's MCP tools come from a single toolSelection list on the
session row. Each entry references a tool provider (or a specific tool
from a provider) — the platform's tool resolver expands the list into
the agent's mcp config at boot and on resume.
Replace the session's tool selection
PATCH /sessions/{id}/tools
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
selection | array | Yes | Ordered list of ToolSelectionEntry. Replaces the row atomically. |
Each ToolSelectionEntry is one of:
1// All tools from a provider
2{ "kind": "provider", "providerKind": "mcp_server", "resourceId": "<uuid>" }
3
4// A single tool from a provider
5{
6 "kind": "tool",
7 "providerKind": "org_database_explorer",
8 "resourceId": "<database-id>",
9 "toolName": "run-query"
10}Closed-set providerKind values: mcp_server, catalog,
org_database_explorer.
Each entry's resourceId is validated against the session's
(orgId, projectId) scope at PATCH time. Cross-org references are
rejected with 403.
Read the current tool selection
The current toolSelection is returned on GET /sessions/{id} as
session.toolSelection: ToolSelectionEntry[].
The resolved set of MCP servers (after expansion through the tool resolver) lands in the agent runtime's MCP config on the next boot or resume.
Skill Attachments
Attach Skill
POST /sessions/{id}/skill-attachments
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
skillSlug | string | Yes | Slug of the skill to attach |
List Skill Attachments
GET /sessions/{id}/skill-attachments
Detach Skill
DELETE /sessions/{id}/skill-attachments/{attachmentId}
File Attachments
Add Attachment
POST /sessions/{id}/attachments
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | kb_page | uploaded_file | internal_url | repo_ref | org_document |
externalRef | string | Yes | Reference to the external resource |
metadata | object | No | Display name, size, mime type, etc. |
List Attachments
GET /sessions/{id}/attachments
Remove Attachment
DELETE /sessions/{id}/attachments/{attachmentId}
Preview Apps
List Preview Apps
GET /sessions/{id}/preview-apps
Response (200):
1{
2 "data": [
3 {
4 "id": "preview-123",
5 "kind": "next_dev",
6 "port": 3000,
7 "status": "running",
8 "url": "https://preview.xema.dev/s/abc123/3000",
9 "startedAt": "2026-04-27T10:35:00Z"
10 }
11 ]
12}Error Codes
| Code | HTTP Status | Description |
|---|---|---|
SESSION_NOT_FOUND | 404 | Session does not exist, or is not visible to the caller |
SESSION_ORG_CONCURRENCY_LIMIT_REACHED | 429 | The org is at its concurrent-session ceiling |
UNAUTHORIZED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Insufficient permissions |
A turn already in flight is refused with the wire contract { "error": "session_busy" } — a distinct shape, not a code in the table above, because a busy session is a retryable state rather than a failure.
A launch that does not complete does not produce an HTTP code. It records a structured LaunchFailureKind on the session, which is what the UI and any automation should branch on. The set is closed and includes, among others:
| Kind | Meaning |
|---|---|
config_resolution_failed | The session's configuration could not be resolved |
launch_selection_invalid | The requested launch selection was not valid |
worker_acquire_timeout | No worker became available inside the deadline |
worker_acquire_rejected | The pool refused the acquisition |
worker_allocation_lost | An acquired worker was lost before the session started |
provision_failed | Provisioning failed |
bootstrap_failed / bootstrap_contract_broken | The runtime started but did not honour the bootstrap contract |
workspace_restore_failed / workspace_snapshot_corrupt | The workspace could not be restored |
no_usable_provider / agent_unavailable | No model provider or agent was usable |
deadline_exceeded | The launch exceeded its deadline |
illegal_transition / launch_abandoned | The launch left a legal state machine path |
launch_unclassified | Failure with no classification — reported as such, never silently mapped |
launch_unclassified exists on purpose: a failure the platform cannot classify is reported as unclassified rather than folded into the nearest plausible neighbour.
Previous: Features
Next: Pipeline Integration