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/pausecancel 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
| Route | What it does |
|---|---|
POST /sessions | Start a session |
GET /sessions | List your sessions |
GET /sessions/{id} | Read a session |
DELETE /sessions/{id} | End a session |
GET /models | List the models you may pick |
GET /repos | Find repositories you may use |
Messages
| Route | What it does |
|---|---|
POST /sessions/{id}/messages | Send a message |
GET /sessions/{id}/pending | List 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}/attachments | Attach a file |
GET /sessions/{id}/attachments/{aid} | Download an attachment |
Following
| Route | What it does |
|---|---|
GET /sessions/{id}/transcript | Read the conversation |
GET /sessions/{id}/events | Follow a session live |
GET /sessions/{id}/status | Read what a session is doing |
GET /sessions/{id}/usage | Read what a session spent |
GET /sessions/{id}/children | List subagent sessions |
GET /sessions/{id}/sandbox | Read where its sandbox is |
GET /sessions/{id}/screen | Watch the sandbox's screen |
GET /sessions/{id}/control | Who controls the screen |
GET /sessions/{id}/decisions | List the decisions people made in a session |
Approvals and questions
| Route | What it does |
|---|---|
GET /sessions/{id}/approvals | List waiting approvals |
POST /sessions/{id}/approvals/{key} | Approve or deny a step |
GET /sessions/{id}/asks | List the agent's questions |
POST /sessions/{id}/asks/{askId} | Answer the agent's question |
Changing a session
| Route | What it does |
|---|---|
POST /sessions/{id}/control | Take over the screen |
POST /sessions/{id}/control/return | Hand the screen back |
POST /sessions/{id}/screen/input | Click or type on the screen |
POST /sessions/{id}/retry | Run a step again |
POST /sessions/{id}/branch | Branch a session |
POST /sessions/{id}/model | Switch a session's model |
POST /sessions/{id}/cancel | Stop the turn |
GET /sessions/{id}/parked | List steps stopped after repeated crashes |
POST /sessions/{id}/parked/retry | Retry steps stopped after repeated crashes |
POST /sessions/{id}/pause | Pause the agent |
DELETE /sessions/{id}/pause | Resume the agent |
POST /sessions/{id}/title | Rename the session |
Sharing
| Route | What it does |
|---|---|
GET /sessions/{id}/shares | List who a session is shared with |
POST /sessions/{id}/shares | Share a session with a person |
DELETE /sessions/{id}/shares/{who} | Stop sharing with a person |
POST /sessions/{id}/links | Make a read-only link |
DELETE /sessions/{id}/links/{linkId} | Revoke a link |
GET /shared/{token} | Read a session through a link |
Flows
| Route | What it does |
|---|---|
GET /flows | List flows |
PUT /flows/{flowId} | Build or change a flow |
DELETE /flows/{flowId} | Remove a built flow |
GET /flows/{flowId}/runs | List a flow's runs |
POST /flows/{flowId}/runs | Start a flow run |
Connections
| Route | What it does |
|---|---|
GET /connections | List your MCP connections |
GET /.well-known/mcp-client | Pistis's OAuth client metadata document |
POST /connections/mcp/{server}/connect | Begin 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
| Route | What it does |
|---|---|
GET /admin/api/people | List people and their roles |
PUT /admin/api/people/{id}/role | Give a person a role |
GET /admin/api/roles | List roles, and the permissions a role can hold |
POST /admin/api/roles | Make a role |
PATCH /admin/api/roles/{roleId} | Rename a role or change its permissions |
DELETE /admin/api/roles/{roleId} | Delete a role |
MCP
| Route | What it does |
|---|---|
GET /.well-known/oauth-protected-resource/mcp | Protected resource metadata for /mcp (RFC 9728) |
GET /.well-known/oauth-authorization-server | Authorization server metadata (RFC 8414) |
GET /oauth/authorize | Begin an authorization (the person's browser) |
POST /oauth/token | Exchange a code or a refresh token for tokens |
POST /oauth/register | Register a client dynamically (RFC 7591) |
POST /oauth/revoke | Revoke an access or refresh token (RFC 7009) |
Webhooks
| Route | What it does |
|---|---|
POST /hooks/{key} | Deliver a webhook |
POST /code-hosts/{connection}/events | Deliver a code host's webhook |
Server
| Route | What it does |
|---|---|
GET /healthz | Check the server is up |
GET /openapi.json | This document |
MCP clients use /mcp instead: the same sessions as tools (Drive Pistis from an agent).
Errors
Errors are { error, code }.
| Status and code | When |
|---|---|
400 invalid | A missing input or idempotencyKey, a body that isn't a JSON object, or an approved that isn't a boolean |
| 401 | A missing or wrong token |
403 forbidden | Someone else's session |
404 unknown_session | No such session |
409 session_ended | A message to an ended session |
409 busy | A retry while a turn runs or a message waits |
| 413 | A body over 1 MiB, or a file over its limit (too_large) |
Events
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
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.
| Header | Value |
|---|---|
x-pistis-signature | sha256= and the hex HMAC-SHA256 of the raw body with the secret. Missing or wrong: 401. |
x-pistis-delivery | A 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.