Pistis

HTTP API

Start and follow sessions, answer approvals and send webhooks over HTTP.

Everything the web app does, you can do over HTTP. Send an API token as Authorization: Bearer <token> on every route except /healthz and /hooks. A token sees only its own person's sessions.

Try it

With pnpm pistis dev running, use the token it printed:

$ export PISTIS_URL=http://127.0.0.1:7777 PISTIS_TOKEN=<token>
$ curl -s -H "authorization: Bearer $PISTIS_TOKEN" -d '{"requestId":"my-first"}' $PISTIS_URL/sessions
{"sessionId":"ses_6c1f2a…","requestId":"my-first","principal":"http","meta":{},"skills":{},"createdAt":1792290735374}
$ pnpm pistis attach ses_6c1f2a…
list the files in /workspace
[1] user: list the files in /workspace
[4] tool-result: total 8 …

requestId is any string you choose. Sending the same one again returns the same session. On a team server, create a token under API tokens.

Control a running session

The same controls as the web app's session page. Each call names the session, so S=$PISTIS_URL/sessions/<id> below, and -H "authorization: Bearer $PISTIS_TOKEN" goes on every one.

Steer it, or queue the next task

$ curl -s $S/messages -d '{"input":"Use the staging database instead","idempotencyKey":"m-2","whenBusy":"steer"}'
{"submissionId":"sub_9a3e…"}
$ curl -s $S/pending
{"items":[{"submissionId":"sub_9a41…","mode":"followUp","text":"Then update the changelog", …}]}
$ curl -s -X DELETE $S/pending/sub_9a41…

While a turn runs, a message waits for it to end (whenBusy: "followUp", the default) or joins it at the next step ("steer"). /pending lists the messages still waiting, oldest first; DELETE one to withdraw it before a turn takes it (409 once taken). mode sets how one turn runs: agent (the default), plan (nothing that could change something runs) or auto. In an auto turn, risky steps are approved as they come, recorded as yours. It's refused unless your server allows auto mode, and only someone who may decide approvals can send it. The next turn asks again.

Stop, pause and resume

$ curl -s -X POST $S/cancel
{"ok":true}
$ curl -s -X POST $S/pause
$ curl -s -X DELETE $S/pause

cancel stops the turn and drops the waiting messages; on a flow's own session it also stops the run and every session it started. pause is gentler: the step running finishes, no new one starts, and messages sent meanwhile wait until DELETE /pause resumes it. A pause survives a server restart.

Answer approvals and questions

curl -s $S/approvals
curl -s $S/approvals/17 -d '{"approved":true}'
curl -s $S/asks/ask_3 -d '{"answer":"Use port 8080"}'

A risky step waits until someone with the right to decide answers it; a second answer gets 409 answered. Questions the agent asks you work the same way under /asks. Every answer is kept, with who gave it, under /decisions.

Change course

curl -s $S/model -d '{"model":"anthropic/claude-sonnet-4-5","thinking":"low"}'
curl -s $S/retry -d '{"toolCallId":"call_12","idempotencyKey":"r-1"}'
curl -s $S/branch -d '{"atSeq":8}'
curl -s $S/title -d '{"title":"Raise the login timeout"}'

model switches from the next model call on, among the models your server allows. retry runs one earlier step again with the same arguments, only while the session is idle (409 busy otherwise). branch starts a new session from the conversation up to entry atSeq, with the files as they were then. title renames it; a name a person gives isn't replaced by the agent's own.

After a crash

A step a crash cut off five times in a row is stopped rather than tried forever. GET $S/parked lists them and POST $S/parked/retry runs them again. A step that was cut off while it ran is reported as outcome unknown and never run twice.

Take over the screen

When the agent uses a browser or desktop: GET $S/screen streams it, POST $S/control takes over (the agent waits), POST $S/screen/input clicks and types, and POST $S/control/return hands it back with an optional note.

Routes

Every route below is in the OpenAPI document (GET /openapi.json on your server serves the same file): load it into any OpenAPI tool to generate a client or try the routes.

Sessions

RouteWhat it does
POST /sessionsStart a session
GET /sessionsList your sessions
GET /sessions/{id}Read a session
DELETE /sessions/{id}End a session
GET /modelsList the models you may pick
GET /reposFind repositories you may use

Messages

RouteWhat it does
POST /sessions/{id}/messagesSend a message
GET /sessions/{id}/pendingList waiting messages
DELETE /sessions/{id}/pending/{sid}Withdraw a waiting message
GET /sessions/{id}/submissions/{sid}Wait for a message's turn
POST /sessions/{id}/attachmentsAttach a file
GET /sessions/{id}/attachments/{aid}Download an attachment

Following

RouteWhat it does
GET /sessions/{id}/transcriptRead the conversation
GET /sessions/{id}/eventsFollow a session live
GET /sessions/{id}/statusRead what a session is doing
GET /sessions/{id}/usageRead what a session spent
GET /sessions/{id}/childrenList subagent sessions
GET /sessions/{id}/sandboxRead where its sandbox is
GET /sessions/{id}/screenWatch the sandbox's screen
GET /sessions/{id}/controlWho controls the screen
GET /sessions/{id}/decisionsList the decisions people made in a session

Approvals and questions

RouteWhat it does
GET /sessions/{id}/approvalsList waiting approvals
POST /sessions/{id}/approvals/{key}Approve or deny a step
GET /sessions/{id}/asksList the agent's questions
POST /sessions/{id}/asks/{askId}Answer the agent's question

Changing a session

RouteWhat it does
POST /sessions/{id}/controlTake over the screen
POST /sessions/{id}/control/returnHand the screen back
POST /sessions/{id}/screen/inputClick or type on the screen
POST /sessions/{id}/retryRun a step again
POST /sessions/{id}/branchBranch a session
POST /sessions/{id}/modelSwitch a session's model
POST /sessions/{id}/cancelStop the turn
GET /sessions/{id}/parkedList steps stopped after repeated crashes
POST /sessions/{id}/parked/retryRetry steps stopped after repeated crashes
POST /sessions/{id}/pausePause the agent
DELETE /sessions/{id}/pauseResume the agent
POST /sessions/{id}/titleRename the session

Sharing

RouteWhat it does
GET /sessions/{id}/sharesList who a session is shared with
POST /sessions/{id}/sharesShare a session with a person
DELETE /sessions/{id}/shares/{who}Stop sharing with a person
POST /sessions/{id}/linksMake a read-only link
DELETE /sessions/{id}/links/{linkId}Revoke a link
GET /shared/{token}Read a session through a link

Flows

RouteWhat it does
GET /flowsList flows
PUT /flows/{flowId}Build or change a flow
DELETE /flows/{flowId}Remove a built flow
GET /flows/{flowId}/runsList a flow's runs
POST /flows/{flowId}/runsStart a flow run

Connections

RouteWhat it does
GET /connectionsList your MCP connections
GET /.well-known/mcp-clientPistis's OAuth client metadata document
POST /connections/mcp/{server}/connectBegin connecting your account to an MCP server with OAuth
PUT /connections/mcp/{server}Connect your account to an MCP server with a pasted token
DELETE /connections/mcp/{server}Disconnect from an MCP server

People and roles

RouteWhat it does
GET /admin/api/peopleList people and their roles
PUT /admin/api/people/{id}/roleGive a person a role
GET /admin/api/rolesList roles, and the permissions a role can hold
POST /admin/api/rolesMake a role
PATCH /admin/api/roles/{roleId}Rename a role or change its permissions
DELETE /admin/api/roles/{roleId}Delete a role

MCP

RouteWhat it does
GET /.well-known/oauth-protected-resource/mcpProtected resource metadata for /mcp (RFC 9728)
GET /.well-known/oauth-authorization-serverAuthorization server metadata (RFC 8414)
GET /oauth/authorizeBegin an authorization (the person's browser)
POST /oauth/tokenExchange a code or a refresh token for tokens
POST /oauth/registerRegister a client dynamically (RFC 7591)
POST /oauth/revokeRevoke an access or refresh token (RFC 7009)

Webhooks

RouteWhat it does
POST /hooks/{key}Deliver a webhook
POST /code-hosts/{connection}/eventsDeliver a code host's webhook

Server

RouteWhat it does
GET /healthzCheck the server is up
GET /openapi.jsonThis document

MCP clients use /mcp instead: the same sessions as tools (Drive Pistis from an agent).

Errors

Errors are { error, code }.

Status and codeWhen
400 invalidA missing input or idempotencyKey, a body that isn't a JSON object, or an approved that isn't a boolean
401A missing or wrong token
403 forbiddenSomeone else's session
404 unknown_sessionNo such session
409 session_endedA message to an ended session
409 busyA retry while a turn runs or a message waits
413A body over 1 MiB, or a file over its limit (too_large)

Events

event stream
id: 42
event: entry
data: {"seq":42,"kind":"tool-result","toolCallId":"…","toolName":"bash","isError":false,"text":"42 passing"}

event: approval
data: {"kind":"approval","key":"17","tool":"bash","args":"{\"command\":\"git push\"}","requestedAt":1792290735374}

Conversation entries arrive as entry events, with kind one of user, assistant, tool-result, system or other. In a browser, use addEventListener("entry", …) on an EventSource.

The event id counts entries from 1. If your connection drops, reconnect with Last-Event-ID or ?after=N. You get every later entry exactly once, in order. Waiting approvals come first as approval events, and again with decided once someone decides.

The reply as it streams

reply events
event: reply
data: {"kind":"reply","id":"r7-1","offset":0,"text":"Ran the tests: 42 pass"}

event: reply
data: {"kind":"reply","id":"r7-1","offset":22,"text":"ing, one skipped."}

While the model writes, reply events carry its text as it streams, about twelve times a second at most. id names one in-progress reply; text goes at character offset of what you have for that id, so the stream grows with the reply, not with its square. Offset 0 is the whole text so far: the first reply event of a stream (connect mid-reply and you get a snapshot), and, with an empty text, that the reply was dropped before an entry (a retry, a stop). The assistant entry ends the partial: its text is the whole reply, so replace what you showed with it. Reply events are live only: they are never entries, never replayed, and nothing of them is written to the transcript. If what you have for an id isn't offset long, drop it and wait for the entry.

Webhooks

A webhook continues every session that called wait_for with its key, with the body as the next message. Turn webhooks on with hookSecret in pistis.json, or PISTIS_HOOK_SECRET with pistis dev.

HeaderValue
x-pistis-signaturesha256= and the hex HMAC-SHA256 of the raw body with the secret. Missing or wrong: 401.
x-pistis-deliveryA unique id per event. A repeat is answered duplicate and does nothing. Missing: 400.

GitHub's x-hub-signature-256 and x-github-delivery headers work as they are, so a GitHub webhook can point straight at /hooks/<key>.

body='{"status":"green"}'
sig=$(printf %s "$body" | openssl dgst -sha256 -hmac "$PISTIS_HOOK_SECRET" | cut -d' ' -f2)
curl -X POST $PISTIS_URL/hooks/ci-1234 -H "x-pistis-delivery: $(uuidgen)" \
    -H "x-pistis-signature: sha256=$sig" -d "$body"

A typed client

httpSessions from @pistishq/sdk speaks this API and resumes dropped streams. See Embed the engine.

On this page