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:

FieldTypeRequiredDescription
agentRefstringConditionalPublished Agent slug or slug@version; omit only when a registered sessionDomainKey resolves it
sessionDomainKeystringConditionalDomain projection kind; requires sessionDomainRef
sessionDomainRefstringConditionalOpaque domain resource id; requires sessionDomainKey
titlestringNoHuman-readable session title
modelIdstringNoOverride the default model
repositoryIdstringNoSCM repository to link
branchStrategystringNoauto_create | use_existing | none
budgetLimitnumberNoMax token budget
customConfigobjectNoAdvanced agent config overrides
lifecyclePolicystringNoephemeral (default; pauses when idle) or persistent (never pauses for idleness)
placementstringNoreuse_if_available or force_isolated

Example:

bash
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):

json
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:

ParameterTypeDescription
statusstringFilter by status
pipelineRunIdstringFilter by pipeline run
pagenumberPage number (default 1)
limitnumberMax results per page (default 20)

Example:

bash
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):

json
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):

json
1{
2  "resumed": true
3}

Fork Session

POST /sessions/{id}/fork

Request Body:

FieldTypeRequiredDescription
titlestringNoTitle 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):

json
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:

FieldTypeRequiredDescription
titlestringNoHuman-readable thread title (defaults to New thread)
agentSlugstringNoPer-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:

FieldTypeRequiredDescription
titlestringNoNew title
statusstringNoactive | 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:

HeaderDescription
Idempotency-Key(Recommended) Unique key to prevent duplicate sends

Request Body:

FieldTypeRequiredDescription
contentstringYesMessage content (max 100KB)

Example:

bash
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.

text
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:

ParameterTypeDescription
limitnumberMax messages (default 50)
cursorstringPagination cursor

Response (200):

json
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:

FieldTypeRequiredDescription
answerstringYesYour 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:

FieldTypeRequiredDescription
grantedbooleanYesWhether 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:

ParameterTypeDescription
typestringFilter by event type
afterSeqnumberGet events after sequence number
limitnumberMax results

Response (200):

json
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):

json
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:

FieldTypeRequiredDescription
branchNamestringYesBranch name to create

Commit Changes

POST /sessions/{id}/git/commit

Request Body:

FieldTypeRequiredDescription
messagestringYesCommit message
filesstring[]NoSpecific files to commit (default: all staged)

Create Pull Request

POST /sessions/{id}/git/pull-requests

Request Body:

FieldTypeRequiredDescription
titlestringYesPR title
descriptionstringNoPR description
baseBranchstringNoTarget branch (default: main)

Check Merge Status

GET /sessions/{id}/git/merge-check?baseBranch=main

Response (200):

json
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:

FieldTypeRequiredDescription
selectionarrayYesOrdered list of ToolSelectionEntry. Replaces the row atomically.

Each ToolSelectionEntry is one of:

jsonc
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:

FieldTypeRequiredDescription
skillSlugstringYesSlug 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:

FieldTypeRequiredDescription
kindstringYeskb_page | uploaded_file | internal_url | repo_ref | org_document
externalRefstringYesReference to the external resource
metadataobjectNoDisplay 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):

json
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

CodeHTTP StatusDescription
SESSION_NOT_FOUND404Session does not exist, or is not visible to the caller
SESSION_ORG_CONCURRENCY_LIMIT_REACHED429The org is at its concurrent-session ceiling
UNAUTHORIZED401Missing or invalid token
FORBIDDEN403Insufficient 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:

KindMeaning
config_resolution_failedThe session's configuration could not be resolved
launch_selection_invalidThe requested launch selection was not valid
worker_acquire_timeoutNo worker became available inside the deadline
worker_acquire_rejectedThe pool refused the acquisition
worker_allocation_lostAn acquired worker was lost before the session started
provision_failedProvisioning failed
bootstrap_failed / bootstrap_contract_brokenThe runtime started but did not honour the bootstrap contract
workspace_restore_failed / workspace_snapshot_corruptThe workspace could not be restored
no_usable_provider / agent_unavailableNo model provider or agent was usable
deadline_exceededThe launch exceeded its deadline
illegal_transition / launch_abandonedThe launch left a legal state machine path
launch_unclassifiedFailure 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