SDKs
Quickstart: from key to first governed tool call
Five minutes, in TypeScript or Python. By the end, your agent asks Immiscible before a tool runs, waits for a person when one is asked, runs the tool only if allowed, records what happened, and hands over a receipt anyone can check.
Both SDKs have no runtime dependencies. Both fail closed: if Immiscible cannot be reached, the tool does not run.
#1. Get an agent key (one minute)
The quickest way is one command in your project: npx immiscible init signs you in, creates the agent and its rule, and writes both variables below to .env. See the CLI guide. To do it by hand, in the console:
- Agents, then Add an agent. Pick what it does (for this guide, Something else), its limits, and who approves; give it a name you will recognise on your phone.
- Open the agent’s setup page and choose Collect the agent’s key. The agent key (
ask_...) is shown once. It can ask for permission and nothing else. - Agents, then Agent limits, then Add a rule, from a template. For this guide take Coding agent (tool calls; anything that reaches the network may only go to the domains you name) and Groceries (payments at tesco.com, sainsburys.co.uk, ocado.com and three more; asks you above £80).
A new agent starts at the workspace’s starting tier: intern unless an owner has chosen junior in the console. An intern has a person sign off every payment, data release and outside action until it has earned more autonomy, so expect its first calls to ask for approval; that is the system working.
export IMMISCIBLE_URL=https://immiscible.fly.dev # the hosted service; your own server if you run one
export IMMISCIBLE_AGENT_KEY=ask_...No server yet? Every example below runs against a built-in fake with --demo.
#2. Install (thirty seconds)
TypeScript (@immiscible/sdk on npm: Node 18+, Deno, Bun, browsers and edge; ESM and CommonJS):
npm install @immiscible/sdkPython (immiscible on PyPI: Python 3.9+, standard library only), in a virtual environment:
python3 -m venv .venv && . .venv/bin/activate
pip install immiscible#3. Ask before a tool runs (two minutes)
A run is one task: one trace and one session shared by everything the agent does for it. guard asks, waits for a person if asked, runs your function only if allowed, then settles.
TypeScript
import { Immiscible, toolAction } from '@immiscible/sdk';
const immiscible = new Immiscible(); // IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL
const run = immiscible.run(); // one trace, one session, per task
const page = await run.guard(
toolAction('fetch_page', { url: 'https://github.com/acme/api' }), // a tool.call action
async () => fetchPage('https://github.com/acme/api'), // runs only if allowed
);Python
from immiscible import Immiscible, tool_action
immiscible = Immiscible() # IMMISCIBLE_AGENT_KEY and IMMISCIBLE_URL
run = immiscible.run() # one trace, one session, per task
with run.guard(tool_action("fetch_page", {"url": "https://github.com/acme/api"})) as decision:
page = fetch_page("https://github.com/acme/api") # runs only if allowedIf the answer is no, the TypeScript call throws ImmiscibleDeniedError and Python raises it; err.reasons says why in plain English and err.signals lists the risk signals. Your code never ran.
#4. When a person must approve (one minute)
Above a mandate’s threshold, the answer is approval_required and a person is asked on their phone. guard waits, backing off between polls (500 ms growing to 8 s, with jitter), for up to ten minutes by default.
TypeScript
const order = await run.pay(
{ amount: 9500, currency: 'GBP', merchant: 'ocado.com', provenance: [{ source: 'user', detail: 'weekly shop' }] },
(decision) => checkout(cart, decision.receipt),
{ onApprovalRequired: (d) => notify(`Approve at ${d.approval.url}`), timeoutMs: 5 * 60_000 },
);Python
payment = Immiscible.payment_action(9500, "GBP", "ocado.com", provenance=[{"source": "user", "detail": "weekly shop"}])
with run.guard(payment, on_approval_required=lambda d: notify(f"Approve at {d.approval_url}"), timeout=300) as decision:
checkout(cart, decision.receipt)Amounts are in minor units: 9500 is £95.00. To wait yourself, call authorize, then waitForDecision(id, { signal }) / wait_for_decision(id, cancel=event); an abort signal or a cancel event stops the wait without touching the approval.
#5. Check the receipt (thirty seconds)
An allowed action carries a receipt: a compact JWS signed with Ed25519. Whoever receives the order checks it, offline against keys you pinned, and once online for single use.
TypeScript
import { fetchJwks, verifyReceipt } from '@immiscible/sdk';
const jwks = await fetchJwks(process.env.IMMISCIBLE_URL); // once; store it with your config
const r = await verifyReceipt(receipt, { jwks, issuer: process.env.IMMISCIBLE_URL, online: true,
expect: { amount: 9500, currency: 'GBP', merchant: 'ocado.com' } });
if (!r.valid) refuse(r.message); // r.reason: expired, bad_signature, replayed, ...Python
from immiscible import fetch_jwks, verify_receipt
jwks = fetch_jwks(url) # once; store it with your config
r = verify_receipt(receipt, url, jwks=jwks, online=True, expect={"amount": 9500, "currency": "GBP", "merchant": "ocado.com"})
if not r.valid:
refuse(r.message)#6. Run it
The demo ships with both SDKs from version 0.1.1, so it runs from an install, against a built-in fake:
npx -p @immiscible/sdk immiscible-demo
python3 -m immiscible.demo # with the virtual environment activeAdd --live to run the same steps against your own workspace (IMMISCIBLE_URL and IMMISCIBLE_AGENT_KEY). In a clone of the repository, the same code is packages/immiscible-js/examples/quickstart.mjs and packages/immiscible-py/examples/quickstart.py.
Both print an allowed tool call, a payment a person approved, a valid receipt, and a prompt-injected lookalike (0cado.com) refused before any code ran.
#What the SDK does for you
| Idempotency | Every authorize carries an idempotency key, in the body and the Idempotency-Key header, reused on every retry. A retry never authorises twice. Framework integrations key on the model’s tool call id. |
| Retries | Network errors, 429 and 5xx are retried twice with backoff (honouring Retry-After). A settle retried after a lost answer returns the settled action, not a 409. |
| Trace | Every request carries a W3C traceparent: the run’s trace id, a fresh span. Pass traceparent to run() to continue your own OpenTelemetry trace. The server’s span comes back in run.context.lastServerTraceparent. |
| Session | Model calls through the gateway and action requests share one session, so the gate can compare what the agent declares with what entered the model’s context. Leave the id out and the gateway issues one (x-immiscible-session, bound to your key). |
| Fail closed | No answer means no action. Do not catch the error and carry on. |
#Next
- Route model calls through the gateway in the same run: gateway helpers
- Framework integrations: OpenAI Agents SDK, LangChain and LangGraph, Vercel AI SDK, Claude Code, MCP clients