API reference
Authentication
Five kinds of caller, never mixed. Each route accepts exactly one, and a credential presented on a route that does not take it is not even read.
| Kind | Looks like | Sent as | Who holds it |
|---|---|---|---|
| Agent key | ask_... (or aat_... from OAuth) | Authorization: Bearer | one agent |
| Workspace key | ask_... | Authorization: Bearer or x-api-key | a person, service or team’s model clients |
| Service token | ims_... | Authorization: Bearer | a machine: SOAR, CI, Terraform |
| Session cookie | __Host-sid | cookie, plus x-immiscible-csrf | a signed-in person |
| Signature | HMAC or JWS | a header or the body | an issuer, identity provider, chat platform |
Every key and token is shown once, when it is made, and stored only as a hash.
#Agent keys
An agent key is a gateway key with the agent scope, bound to exactly one agent. It may:
- ask the gate (
POST /v1/actions/authorize), poll its own actions and settle them; - connect to the MCP server and the MCP proxy;
- carry that agent’s inference through the gateway.
It can never approve, create or widen a mandate, unfreeze itself or read the vault: none of those exist on any route a key can reach. MCP clients that connect by OAuth get an access token (aat_...) that resolves to the same agent. A stopped agent’s key is refused everywhere Immiscible enforces: its action requests are denied (agent_frozen), and its model calls and proxied tool calls get 403 agent_stopped before anything is sent (what a stop stops).
curl "https://immiscible.fly.dev/v1/actions/act_7Qm2c1f0" -H "authorization: Bearer $IMMISCIBLE_AGENT_KEY"#Workspace keys
Issued under Settings, For engineers, API keys in the console, or with an admin key via POST /v1/admin/keys. A key binds traffic to a principal (a person or service), a team and optionally a default task class and policy profile. Scope is inference (the default) or admin, which can also read reports, evidence packs and upstream status and set budgets.
curl "https://immiscible.fly.dev/v1/models" -H "authorization: Bearer $IMMISCIBLE_KEY"Model SDKs send the key in their own way (x-api-key for Anthropic’s); both work.
#Service tokens
For machines that operate Immiscible: a SOAR playbook, a CI pipeline, Terraform. Made by an owner under Settings, scoped, and recorded in the ledger as themselves, by name and id, so the record says “token:Splunk SOAR froze 40 agents”, not a person’s name.
| Scope | Lets a token |
|---|---|
agents:read | list agents and their standing |
agents:write | register agents from a blueprint, with mandates from templates only |
agents:freeze | freeze one agent or a fleet, and run drills |
approvals:read | read waiting approvals and suggested rules |
evidence:read | the evidence bundle and the signed scorecard |
No scope decides an approval: a machine approving is not a person approving. No token lifts a freeze. Every token expires (90 days at most) and may be pinned to address ranges.
curl -X POST "https://immiscible.fly.dev/api/w/$IMMISCIBLE_WORKSPACE/service-tokens" \
-H "cookie: __Host-sid=$IMMISCIBLE_SESSION" -H "x-immiscible-csrf: 1" \
-H "content-type: application/json" \
-d '{ "name": "Splunk SOAR", "scopes": ["agents:read", "agents:freeze"], "expiresInDays": 90, "ipAllow": ["203.0.113.0/24"] }'curl "https://immiscible.fly.dev/v1/admin/agents" -H "authorization: Bearer $IMMISCIBLE_SERVICE_TOKEN"#Session cookies
The console’s routes under /api/ authenticate a signed-in person by the session cookie (__Host-sid in production, sid in development), HttpOnly, Secure and SameSite=Lax. Bearer tokens are ignored on them entirely.
Workspace routes name the workspace: /api/w/$IMMISCIBLE_WORKSPACE/.... Signed in, these pages fill in your workspace’s id; otherwise they use the shell variable IMMISCIBLE_WORKSPACE, and GET /api/me lists your workspaces’ ids:
export IMMISCIBLE_SESSION=... # the value of the session cookie, from your browser's developer tools
export IMMISCIBLE_WORKSPACE=$(curl -s "https://immiscible.fly.dev/api/me" -H "cookie: __Host-sid=$IMMISCIBLE_SESSION" | sed -E 's/.*"workspaces":\[\{"id":"([^"]+)".*/\1/')On a local http server the cookie is called sid; the docs’ __Host-sid is accepted there too.
Every state-changing request (anything but GET and HEAD) must also carry x-immiscible-csrf: 1, a header a cross-site form cannot send, and where the browser sends an Origin it must match the deployment. Without both, the answer is 403 csrf.
The member’s role decides what they may do: owners and admins manage everything, members manage agents that act for them, analysts read reports, auditors read evidence. Signing in can require two-factor or single sign-on: see identity and access.
#Signed requests
Inbound calls from other systems carry no bearer credential. They prove themselves by signing the raw body, and anything that does not verify is refused before the body is read.
| From | Header | Scheme |
|---|---|---|
| Card issuer (generic) | immiscible-signature | t=<unix>,v1=<hex HMAC-SHA256 of "t.body">, issuer secret, five minutes |
| Stripe Issuing, Stripe billing | stripe-signature | Stripe’s own, with the saved signing secret |
| GitHub | x-hub-signature-256 | GitHub’s own, with the integration secret |
| Identity provider (SSF) | none: the body is a SET | ES256 or RS256 JWS, issuer, audience, freshness, jti once |
| Slack | x-slack-signature, x-slack-request-timestamp | Slack v0, five minutes, each signature once |
| Teams relay | the relay’s signature header | HMAC with the per-workspace secret, plus a token per card button |
Outbound webhooks Immiscible sends to you use the same immiscible-signature scheme; see SIEM export.
#OAuth for MCP clients
Immiscible is an OAuth 2.1 authorisation server for MCP clients (claude.ai, ChatGPT, Claude Code and others) that connect to /mcp as one agent: discovery at /.well-known/oauth-authorization-server, dynamic client registration, PKCE (S256), refresh tokens and revocation. The person picks the agent on the consent screen. Your MCP client normally does all of this; the walkthrough below does it by hand with curl and openssl, so you can see each step or build a client of your own.
Access tokens (aat_...) last an hour and refresh tokens (art_...) are single use: each refresh returns a new pair, and presenting a used refresh token revokes the whole connection. A connection also stops when the agent is frozen or removed, or the person who connected it leaves the workspace.
#1. Discover and register
curl -s "$IMMISCIBLE_URL/.well-known/oauth-authorization-server"
REDIRECT=http://127.0.0.1:8976/callback
CLIENT_ID=$(curl -s -X POST "$IMMISCIBLE_URL/oauth/register" \
-H "content-type: application/json" \
-d "{\"client_name\": \"My MCP client\", \"redirect_uris\": [\"$REDIRECT\"]}" \
| sed -E 's/.*"client_id":"([^"]+)".*/\1/')
echo "$CLIENT_ID" # mcp_...A redirect URI is https, or http on a loopback address. With no token_endpoint_auth_method the client is public (none) and authenticates by client_id alone; register with client_secret_post or client_secret_basic to be given a client_secret as well.
#2. PKCE
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')#3. The person signs in and chooses an agent
Open this in a browser signed in to Immiscible:
echo "$IMMISCIBLE_URL/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT&code_challenge=$CHALLENGE&code_challenge_method=S256&state=xyz&resource=$IMMISCIBLE_URL/mcp"The consent screen lists the agents this person may connect. Allow sends the browser to the redirect URI with ?code=...&state=xyz&iss=.... Check state and iss, and copy the code:
CODE=... # from the redirect, valid for a few minutes and once#4. Exchange the code
curl -s -X POST "$IMMISCIBLE_URL/oauth/token" \
-d grant_type=authorization_code -d code="$CODE" -d code_verifier="$VERIFIER" \
-d client_id="$CLIENT_ID" -d redirect_uri="$REDIRECT"{ "access_token": "aat_...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "art_...", "scope": "immiscible:agent" }Use the access token at /mcp exactly as an agent key:
curl -s -X POST "$IMMISCIBLE_URL/mcp" -H "authorization: Bearer $ACCESS" -H "content-type: application/json" \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }'#5. Refresh
curl -s -X POST "$IMMISCIBLE_URL/oauth/token" \
-d grant_type=refresh_token -d refresh_token="$REFRESH" -d client_id="$CLIENT_ID"The answer is a new access token and a new refresh token; the old access token stops working.
#6. Revoke
curl -s -X POST "$IMMISCIBLE_URL/oauth/revoke" -d token="$REFRESH" -d client_id="$CLIENT_ID"Revoking either token ends the whole connection: every token issued under it stops working. The answer is 200 {} whatever the token was (RFC 7009). The person can also disconnect the app on the agent’s page in the console.