API Clients
An API client is a machine caller of the session API, such as an eval runner. It exchanges a client secret for a short-lived access token, then creates sessions, starts turns, and reads their events directly from api-rs.
Each client runs its sessions as one of the principals an admin attached to it,
so a session's sandbox gets exactly that principal's grants. A client can only
use thread keys that start with api:<client id>:.
Enable API clients
API clients are off by default. Set apiClients.enabled: true in your Helm
values. While the flag is off, the console hides API Clients, api-rs does
not serve /oauth/token, and api-rs rejects tokens issued earlier. Clients you
created are kept.
Create a client
In the console, open API Clients (admins only) and add a client:
- Scopes:
sessions:writecreates sessions, appends messages, and starts or interrupts turns.sessions:readreads session state and events. - Default principal: an OID or foreign ID. Sessions run as it unless they name another attached principal. Attach more principals on the client's page.
The secret is shown once. Rotate it if it is lost or leaked: rotating rejects the old secret and every access token issued for it on their next use. Disabling a client rejects all of its access tokens the same way.
Get an access token
Expose /oauth/token and /api/session/* on api-rs to the client, for example
through httpRoutes. Keep other api-rs routes internal.
TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
https://api.example.com/oauth/token | jq -r .access_token)Tokens are valid for 1 hour. When one expires, request a new one. You can
pass scope to request a subset of the client's scopes.
Run a session
THREAD_KEY="api:$CLIENT_ID:eval-$(uuidgen)"
THREAD_PATH=$(jq -rn --arg key "$THREAD_KEY" '$key|@uri')
API=https://api.example.com
curl -s -X POST "$API/api/session/$THREAD_PATH" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"harness_type":"codex"}'
curl -s -X POST "$API/api/session/$THREAD_PATH/messages" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","parts":[{"type":"text","text":"Reply with exactly PONG."}]}]}'
EXECUTION_ID=$(curl -s -X POST "$API/api/session/$THREAD_PATH/execute" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"idempotency_key":"eval-1","input_lines":["{\"type\":\"user\",\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"Reply with exactly PONG.\"}]}}"]}' \
| jq -r .execution_id)
curl -s -N -H "Authorization: Bearer $TOKEN" \
"$API/api/session/$THREAD_PATH/events?execution_id=$EXECUTION_ID&after_event_id=0"To pick another attached principal, add "principal": "<OID or foreign ID>"
when creating the session. A session keeps the principal it was created with.
If that principal is detached, the session rejects new turns.
The event stream is replayable. Pass the last event id you received as
after_event_id to resume, and stop at session.execution_completed,
session.execution_failed, or session.execution_cancelled.
An open stream ends with a session.stream_error event when its token expires,
or within 30 seconds of the client being disabled, its secret rotated, or its
sessions:read scope removed. Request a new token and resume from the last
event id.
When a session will not be used again, for example at the end of an eval, release its sandbox instead of waiting for the idle timeout:
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
"$API/api/session/$THREAD_PATH/sandbox"This stops and deletes the sandbox and its state, and needs sessions:write.
It returns 409 while a turn is queued or running. The transcript and events
remain; a later turn starts in a fresh sandbox.
Metadata a client sends with a session or turn is stored under
client_metadata. It is never treated as a Slack user or requester identity.