Skip to content
LogoLogo

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:write creates sessions, appends messages, and starts or interrupts turns. sessions:read reads 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.