Skip to content

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.

KindLooks likeSent asWho holds it
Agent keyask_... (or aat_... from OAuth)Authorization: Bearerone agent
Workspace keyask_...Authorization: Bearer or x-api-keya person, service or team’s model clients
Service tokenims_...Authorization: Bearera machine: SOAR, CI, Terraform
Session cookie__Host-sidcookie, plus x-immiscible-csrfa signed-in person
SignatureHMAC or JWSa header or the bodyan 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).

Shell
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.

Shell
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.

ScopeLets a token
agents:readlist agents and their standing
agents:writeregister agents from a blueprint, with mandates from templates only
agents:freezefreeze one agent or a fleet, and run drills
approvals:readread waiting approvals and suggested rules
evidence:readthe 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"] }'

#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:

Shell
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.

FromHeaderScheme
Card issuer (generic)immiscible-signaturet=<unix>,v1=<hex HMAC-SHA256 of "t.body">, issuer secret, five minutes
Stripe Issuing, Stripe billingstripe-signatureStripe’s own, with the saved signing secret
GitHubx-hub-signature-256GitHub’s own, with the integration secret
Identity provider (SSF)none: the body is a SETES256 or RS256 JWS, issuer, audience, freshness, jti once
Slackx-slack-signature, x-slack-request-timestampSlack v0, five minutes, each signature once
Teams relaythe relay’s signature headerHMAC 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

Shell
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

Shell
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:

Shell
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:

Shell
CODE=...     # from the redirect, valid for a few minutes and once

#4. Exchange the code

Shell
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"
JSON
{ "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:

Shell
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

Shell
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

Shell
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.