API reference
Errors and headers
Errors come back in the shape your client already expects, with a stable type and a message in plain English. Headers that steer the gateway, and the ones it sends back.
#The error shape
{
"error": {
"type": "agent_stopped",
"message": "this agent is stopped under an owner hold, so nothing it sends goes upstream and nothing was sent; the person it acts for, an owner or an admin can restart it in the console under Agents",
"hold": "owner",
"stopped": "agent",
"agentId": "agt_4f2c91a7",
"fix": "Stop: the kill switch is on (error.hold says which hold, error.message who may lift it). Do not retry; tell the person you act for.",
"docs": "https://immiscible.fly.dev/docs/guides/kill-switch#what-a-stop-stops",
"requestId": "d210346a-c44e-4be8-bca7-1d8503868ce8"
}
}requestId is the same id as the x-request-id response header, on every error: quote it when you write to us and we can find the request. Every 4xx carries fix, one sentence saying what to send differently, and docs, the page that explains it, so an agent can correct its next request from the error alone. Both are additions to the shape your client already reads; a route that has its own more specific fix keeps it. On the MCP server, JSON-RPC errors carry them in error.data, and a refused tool call puts them in the tool result’s text.
A validation failure also lists each problem with the field it is about, so a form can mark the field. message joins them with “; “:
{
"error": {
"type": "invalid_request",
"message": "summary is required: say in one sentence what the agent is about to do; payment.amount must be a whole number of minor units (pence) above zero, for example 6420 for £64.20",
"errors": [
{ "field": "summary", "message": "summary is required: say in one sentence what the agent is about to do" },
{ "field": "payment.amount", "message": "payment.amount must be a whole number of minor units (pence) above zero, for example 6420 for £64.20" }
]
}
}field is a dotted path into the body, or null when the problem is the body as a whole. invalid_request, invalid_mandate, invalid_budget and the other validation types (invalid_application, invalid_upstream) carry errors.
Every response, error or not, carries an x-request-id header. It is the id of the request in our logs: quote it when you ask for help with one.
On Anthropic-shaped routes errors use Anthropic’s envelope ({ "type": "error", "error": { ... } }), so SDK error handling keeps working. error.type is stable and safe to branch on; error.message is for people and may change. Some refusals carry the numbers a client needs to act (for example oldCount and newCount on selection_changed). Every refusal that reflects a policy decision carries an evidenceRecord id: the refusal itself is in the ledger.
#Common to every route
| Status | Type | Meaning |
|---|---|---|
| 400 | invalid_json | the body is not JSON |
| 400 | invalid_request | a field is missing or malformed; errors lists each problem |
| 401 | unauthenticated, invalid_api_key | no credential, or one that is unknown, expired or revoked |
| 403 | forbidden | the credential is valid but this caller may not do this; the message says why |
| 403 | csrf | a console write without x-immiscible-csrf, or from another origin |
| 404 | not_found | no such object in this workspace (an id from another workspace is simply not found) |
| 404 | route_not_found | no route has this method and path; didYouMean names the nearest real one, for example POST /v1/actions/authorize |
| 405 | method_not_allowed | the path exists with other methods; the allow header lists them |
| 403, 409 | separation_of_duties, second_owner_required, second_person_required | another person must do this |
| 413 | body_too_large | over the limit: 1 MB for most routes, larger for model traffic and billing imports |
| 429 | rate_limited | honour retry-after |
| 500 | internal | our fault; in production the message says only “internal error”; send us the x-request-id |
#The gate
| Status | Type | What to do |
|---|---|---|
| 400 | invalid_request | no type, no summary, a one-word type that is not built in (paymnt: the message suggests payment; custom types are dotted, such as crm.update), an amount that is not whole minor units, a currency that is not three letters; errors lists each |
| 400 | invalid_status, invalid_amount | a settlement needs completed, failed or cancelled, and whole minor units |
| 402 | plan_limit_reached | the plan’s agent or mandate allowance is used |
| 400 | invalid_mandate | creating or changing a mandate: its limits, merchants, fields or actions are malformed; errors lists each |
| 403 | agent_key_required | the key is not an agent key; agent routes need one (Agents, Add an agent, then Collect the agent’s key on its setup page) |
| 403 | agent_not_found | an agent key whose agent has since been removed |
| 200 | (deny, signal agent_frozen) | the kill switch is on for this agent: a stopped agent’s action requests are decisions, not HTTP errors |
| 404 | not_found | no action with that id belongs to this agent |
| 409 | idempotency_conflict | the key was used for a different request |
| 409 | confirm_broaden | a new rule would allow anywhere (any domain, any merchant) beside a narrower rule for the same agent; reason says which, and sending confirmBroaden: true saves it |
| 409 | not_allowed, already_settled | only an allowed, unsettled action can be settled |
#The gateway
| Status | Type | What to do |
|---|---|---|
| 400 | unclassified_request | send x-immiscible-task-class or bind a default class to the key (enforce mode only) |
| 400 | sensitive_data_blocked | the workspace’s DLP setting is block; the message counts what was found by kind, never the values |
| 402 | approval_required | the budget’s approval step; the body names the owner |
| 402 | seat_limit_reached | Free’s people limit is in use this month, and its seven days of grace have ended |
| 403 | agent_stopped | the key’s agent is stopped, or Stop every agent stands and the key is not bound to one agent; hold names the hold, message who may lift it, and nothing was sent (what a stop stops). The MCP proxy refuses the same way, in error.data |
| 409 | policy_conflict | no model satisfies the policy; relaxations lists what would admit one, at what cost, signed off by whom |
| 429 | budget_exhausted | the budget’s hard ceiling |
| 429 | plan_limit_reached | the plan’s monthly request allowance is used |
| 502 | upstream_failure | every eligible model failed; attemptedModels lists them |
#Machine admin
| Status | Type | What to do |
|---|---|---|
| 401 | invalid_token, token_expired | unknown, revoked or expired, or presented from an address the token is not pinned to |
| 400 | invalid_budget | POST /v1/admin/budgets: a scope, amount, period or ladder that is not valid; errors lists each |
| 403 | missing_scope | the token lacks the scope this route needs |
| 400 | template_only, unknown_template | a token attaches mandates from templates only |
| 400 | invalid_selector, nothing_selected, reason_required | a bulk freeze needs a selector that matches and a reason |
| 409 | confirm_count_required, selection_changed | send confirmCount equal to the current count |
| 429 | drill_cooldown | a token may start one drill an hour per workspace |
| 202 | (pending) | a freeze past a ceiling waits for a person; not an error, but nothing is frozen yet |
#Headers
#Request
| Header | Purpose |
|---|---|
x-immiscible-task-class | what kind of work this is, for example code.feature, support.triage |
x-immiscible-task-id | group this call into an existing task |
x-immiscible-objective | balanced (default), cost, quality, latency or sovereign |
x-immiscible-dry-run | true to route and budget-check without calling a model |
x-immiscible-human-oversight | acknowledged for high-risk work a person reviews |
x-immiscible-client-session | the agent’s session id, 1 to 128 printable characters; name the same id as session.id on action requests |
idempotency-key | the action request’s idempotency key, if not in the body |
traceparent | a W3C trace to continue |
x-immiscible-csrf | 1 on every console write |
#Response
| Header | Meaning |
|---|---|
x-request-id | this request’s id in our logs; quote it when asking for help |
immiscible-version | the API version that answered, as a date (2026-10-01). There is one version; changes within it only add (new operations, optional fields, new values to ignore when unknown). A change that removes or renames anything would come under a new date. |
ratelimit-limit, ratelimit-remaining, ratelimit-reset | on the gate’s routes, every answer: the limit per minute, what is left of it, and seconds until it is full again |
allow | on 405, the methods the path accepts |
traceparent | the trace this request ran in; join your own spans to it |
x-immiscible-session | a server-issued session id for a client that named none; send it back |
x-immiscible-task-id | the task this call was recorded against |
x-immiscible-call-id | this call |
x-immiscible-routed-to | the model that served it (enforce mode) |
x-immiscible-would-route-to | what routing would have chosen (shadow mode) |
x-immiscible-enforcement | shadow, observe, nudge or downgrade |
x-immiscible-cost-micros | cost in millionths of a US dollar, from reconciled usage |
x-immiscible-jurisdiction | serving domicile and data region |
x-immiscible-budget-scope, x-immiscible-yield-multiplier | the budget that bound this request, and its multiplier |
x-immiscible-advisory | a plain-English note when a budget is close |
retry-after | seconds to wait, on 429 |