---
title: POST /orchestrate
description: Execute the biomedical agent
full: true
---

**Auth:** optional. The route accepts anonymous calls; send an `ivy_` API key (`Authorization: Bearer ivy_…` or `X-Ivy-Api-Key`) or a Clerk session JWT (`Authorization: Bearer …`) to call as your own account. See [Authentication](/docs/api/authentication).

Full Ivy agent loop: intent detection, dynamic tool selection, GPU execution via Modal, and multi-step reasoning. Signed-in callers resume a session by passing its `sessionId`; a guest's `sessionId` is ignored and every guest call starts a fresh, unsaved session. Unknown request fields are ignored. Streaming (default) emits named SSE events; non-streaming returns a summary JSON object. Approval gates (cost and safety checks on expensive or risky tools) are not enabled: every tool call proceeds automatically, for web, terminal and API callers alike. `gatesAutoApproved` lists each call that the gates would have stopped and that ran without an approval step. A session runs one turn at a time: a second request on the same session while a turn is running is refused with 409 `TURN_IN_PROGRESS`. Closing the connection stops the turn and cancels the jobs it started (a guest call's too), unless the request sent `x-pleach-on-disconnect: continue`. A turn stopped while the connection is still open (for example at the server time limit) emits a `stopped` event followed by `done`, or `status: "stopped"` in the JSON body.

**Trust and tool output.** Tool results, tool errors and citations carry third-party data: database records, papers, web pages, files and computed outputs. Ivy is instructed to treat that content as data and never to follow instructions inside it. That is an instruction to the model, not a guarantee. Every tool call, `tool_result`, `tool_error` and citation is marked `trust: "untrusted"`, and tool payloads name their `source` (the tool). Treat `untrusted` content as data in your own code and agent too: quote, display or cite it, and never act on instructions found in it.

## Request

#### Headers

- `x-pleach-depth` (integer) —
Delegation depth of this call. Omit it on a first call. An agent that calls this endpoint while running a turn for another agent call must send its own depth plus one, so a chain of agents calling agents stops after 4 hops. A value above 4 is refused with 400 `DELEGATION_DEPTH_EXCEEDED`; a value that is not a single non-negative integer is refused with 400 `INVALID_DELEGATION_DEPTH`. The header is a loop guard you set yourself, not a credential: it grants nothing.

- `x-pleach-on-disconnect` (enum (&quot;error&quot; | &quot;stop&quot; | &quot;continue&quot;)) —
What closing the connection means for the turn (case-insensitive; also accepted as the `on_disconnect` query parameter). Absent or any other value (`error`): the turn stops and the jobs it started are cancelled (a guest call's too). `stop`: the same, recorded as a stop you asked for. `continue`: the turn does not stop; it finishes on the server within the request time limit, its jobs keep running, and nothing more is sent to the closed connection.

#### Request body

- `prompt` (string) —
The user prompt for the biomedical agent

- `model` (string) —
Override the model for this turn. Signed-in callers who omit it get the model saved in their AI preferences, as in the web app. The override must belong to a model family your organization allows; otherwise the call returns 403 `MODEL_FAMILY_NOT_PERMITTED`. Guest calls always run on the guest model.

- `sessionId` (string) —
Resume an existing session (UUID v7). Signed-in callers only: a guest's `sessionId` is ignored and a fresh session is started

- `projectId` (string (uuid)) —
Signed-in callers only. Run the turn inside one of your projects, as a web chat in that project does. A project you cannot access returns 404 `PROJECT_NOT_FOUND`; a project-scoped API key can only name its own projects.

- `campaignId` (string (uuid)) —
Signed-in callers only. Run the turn inside one of your campaigns; the campaign's recent job history is added to the agent's context. A campaign you cannot access, or one outside `projectId`, returns 404 `CAMPAIGN_NOT_FOUND`.

- `fileReferences` (string (uuid)[]) —
Signed-in callers only. Ids of files you uploaded, attached to the prompt the way the web app's file picker attaches them. Any id that is not one of your files returns 404 `FILE_NOT_FOUND`.

- `stream` (boolean) —
Enable SSE streaming (default: true)

- `outputFormat` (enum (&quot;sse&quot; | &quot;text&quot;)) —
Presentation of a streaming turn. `sse` (default) is the wire contract: named `text/event-stream` events, as documented below. `text` writes the same events as labelled plain text (`text/plain`) for someone reading `curl -N` output: boxed `session` / `tool` sections, status glyphs, the answer as prose, and a `done` summary — no SSE framing, colour only on a TTY and only when `NO_COLOR` is unset. The query string form `?format=text` is equivalent, and `?format=sse` pins the default. Content, events and billing are identical either way.

- `diagnostics` (boolean) —
Opt-in headless diagnostics (IH-19): collect console breadcrumbs + a synthesized TurnReport for offline analysis. Default: false.

- `tools` (string[]) —
Allow-list. When present, only these tools may be offered to the model or run, plus a small exempt set the platform needs (`review_tool_registry`, `get_data`, `sandbox_read`). `[]` means no domain tools (exempt set only); omit for no restriction. Names are matched as canonical tool names after alias resolution (e.g. `diffdock` → `diffdock_dock`). Names must be tools this deployment offers: an unknown, deprecated or disabled name is a 400 `UNKNOWN_TOOL` (`details.unknownTools` echoes the first 20, `details.unknownCount` counts them all). At most 256 names of at most 128 characters each; more is a 400 `INVALID_REQUEST`. The list only narrows: it never admits a tool your tier or the guest tier refuses, and sub-agents inherit it.

- `excludeTools` (string[]) —
Deny-list. These tools are never offered to the model and a call to one is refused (tool error `TOOL_EXCLUDED_BY_REQUEST`) — including the exempt set if you name it. Same name matching and `UNKNOWN_TOOL` rule as `tools`. Wins over `tools`. Sub-agents inherit it. Withheld tools are reported in the response's `withheldTools`.

- `maxCredits` (number) —
Most credits this response's model calls may debit. Each model call reserves the size of the largest call so far (at least 1 credit) before it is sent, and is refused when the credits spent plus every reservation of a call still running plus its own would exceed the cap, so calls made in parallel are checked together. Once one more tool round and the answer no longer fit, the tool loop stops and the answer is written from the results gathered; `usage.llm.capped` is then `true`. A call larger than every earlier one can overshoot by the difference. Counted: the model calls this response makes itself. Not counted or capped: GPU job credits (they appear in `usage.gpu`) and model calls a tool makes through a request to another API route. Guests may send it and it is accepted, but it is not applied: a guest call is bounded by the guest limits (the daily call quota and compute budget) instead. Must be a positive number, else 400 `INVALID_REQUEST`.

## Responses

### `200` OK

Agent execution response

Response shape:

- `sessionId` (string) —

- `resumed` (boolean) —

- `status` (enum (&quot;stopped&quot;)) —
Present only when the turn was stopped before it finished (for example at the server time limit); `content` is then the partial answer. Absent on a completed turn.

- `reason` (enum (&quot;user&quot; | &quot;timeout&quot;)) —
Why the turn was stopped. Present only with `status: "stopped"`.

- `content` (string) —
Ivy's full response text

- `toolCalls` (object[]) —

- `artifacts` (object[]) —

- `citations` (object[]) —
Source citations for the answer. Always present; an empty array means none were emitted. Folded from the turn's citation sets: a set marked final replaces the earlier ones, any other set is appended, deduplicated on `source:identifier`.

- `record` (object) —
Where to verify this session's audit record. Authenticated callers only: the key is absent for guest calls, whose sessions are not kept. Paths are relative to the API base URL.

#### record properties

- `chatId` (string) —
The session id (the same value as `sessionId`; kept under this name for existing callers)

- `verify` (object) —

#### verify properties

- `api` (string) —
`/api/audit/session-chain/<sessionId>`, called with an API key or a signed-in session

- `page` (string) —
`/audit/verify?sessionId=<sessionId>`, for a signed-in browser

- `withheldTools` (object[]) —
Tools the request's `tools` / `excludeTools` policy withheld this turn: offered-then-stripped intent or critical tools and refused calls, one entry per tool, at most 20 entries. Always present; an empty array means nothing was withheld.

- `gatesAutoApproved` (object[]) —
Tool calls this turn that an approval gate (cost or safety check) would have stopped and that ran without an approval step, because gates are not enabled. One entry per tool and gate, at most 64 entries. Names only. Always present; an empty array means no call would have been gated.

- `gatesAutoApprovedDropped` (integer) —
Present only when more distinct gated calls ran than `gatesAutoApproved` lists: how many were left off it.

- `usage` (object) —
Credits this response's model calls debited from your balance (`llm`), and the credits held for the GPU jobs it submitted (`gpu`). Authenticated callers only: the key is absent for guest calls, which are not billed. Credits only: no dollar cost and no balance figure. A GPU hold is not the final charge: each job is charged when it finishes.

#### usage properties

- `llm` (object) —

#### llm properties

- `credits` (number) —
Credits debited for this turn's model calls (successful debits only).

- `calls` (integer) —
Model calls this turn attempted to debit.

- `settled` (string) —
`n/m`: n debits succeeded of the m attempted. A refused debit (for example, insufficient credits) counts in m only.

- `final` (boolean) —
True when every attempted debit had finished before the response was sent and no model call of this response was still running. False means a debit was still in flight, or a call is still running (`pendingCalls`); it still completes, and is not reflected here.

- `pendingCalls` (integer) —
Model calls this response started that were still running when it was sent (for example, the post-answer quality check). Each is charged, if at all, when it finishes; its credits are not in `credits`, and it is normally not counted in `calls` (do not add the two together). Omitted when there were none.

- `basis` (enum (&quot;measured&quot; | &quot;estimated&quot; | &quot;fallback-default&quot;)) —
How the credits were priced, worst case first. `fallback-default`: at least one model had no platform price, or its price could not be confirmed, and may have been charged at the default rate. `estimated`: at least one call was charged from estimated token counts (an interrupted or usage-less response). `measured`: every call was charged from the provider's reported usage. Omitted when the turn made no model call.

- `maxCredits` (number) —
The request's `maxCredits`, echoed. Omitted when the request set none.

- `capped` (boolean) —
Present when the request set `maxCredits`. `true` when the ceiling stopped the tool loop early or refused a model call; the answer was written from the results gathered before that.

- `gpu` (object) —
GPU jobs this response's tools submitted, and the credits held for them at submission. A hold is not the final charge: each job is charged when it finishes. Covers jobs submitted through the job service during this turn.

#### gpu properties

- `reservedCredits` (number) —
Sum of the credits held for this turn's jobs. A job with no hold (for example, on an unlimited plan) adds nothing.

- `jobs` (object[]) —
One entry per submitted job, in submission order (at most 64).

- `droppedJobs` (integer) —
Jobs past the 64-entry list. Their holds are in `reservedCredits`. Omitted when 0.

- `final` (boolean) —
`false` when any job was submitted: its charge settles when it finishes, after this response. `true` when no job was submitted.

- `notice` (string) —
A short note on what the figures cover.

- `stepsCompleted` (integer) —

- `messageCount` (integer) —

- `interrupts` (object[]) —
Approval gates the turn raised. Always present; an empty array means none fired. API and MCP callers have no interactive approver, so a gate auto-proceeds (`outcome: auto_proceeded`) and the tool runs.

- `plan` (object) —
The turn's plan. `null` means the turn made no plan; the key is absent when the turn ended before its plan could be read.

- `subagents` (object[]) —
Sub-agents the turn spawned. Always present; empty means none.

- `corrections` (object[]) —
Corrections applied to the answer during the turn. The answer text already reflects them; each entry names why. Always present; empty means none.

- `modelSignals` (object) —
Model retries, fallbacks to another model, and whether every model in the selected family failed.

#### modelSignals properties

- `retries` (object[]) —

- `fallbacks` (object[]) —

- `familyExhausted` (object) —

#### familyExhausted properties

- `family` (string) —

- `attemptedModels` (string[]) —

- `followUp` (object) —
Present when the turn ended while background jobs were still running (signed-in callers only). Poll `pollUrl` (GET /api/v1/orchestrate/&#123;sessionId&#125;) with the same credential; once the jobs finish, the answer composed from their results appears under `resume`. Guests get no handle: a guest session is not saved, so it cannot be fetched later.

#### followUp properties

- `sessionId` (string) —

- `pollUrl` (string) —

- `awaitingJobIds` (string[]) —

### `400` Bad Request

`INVALID_REQUEST`: the body failed validation. `UNKNOWN_TOOL`: `tools` or `excludeTools` named a tool that does not exist. `DELEGATION_DEPTH_EXCEEDED`: `x-pleach-depth` is above 4. `INVALID_DELEGATION_DEPTH`: `x-pleach-depth` is not a single non-negative integer.

Response shape: `object`

### `401` Unauthorized

Authentication required

Response shape: `object`

### `402`

`INSUFFICIENT_CREDITS`: a signed-in caller's credit balance cannot cover the turn. Nothing is run or charged.

Response shape: `object`

### `403` Forbidden

`TOOL_NOT_AVAILABLE_FOR_TIER`: a guest call asked for a tool the guest tier does not include. Nothing is charged.

Response shape: `object`

### `404` Not Found

Session not found (when a signed-in caller resumes with sessionId)

Response shape: `object`

### `409` Conflict

`TURN_IN_PROGRESS`: a turn is already running on this session (one turn at a time per session and caller). Nothing is run or charged. Retry after the `Retry-After` header (seconds); the running turn may take longer.

Response shape: `object`

### `429` Too Many Requests

>
> Guest limit reached: `GUEST_QUOTA_EXCEEDED` (18 calls per day per IP) or `GUEST_COST_LIMIT` ($1.00 of compute per day per IP, metered from the measured usage of Ivy's model calls). Both reset at UTC midnight. The agent call limits below also apply.
>
> `RATE_LIMITED`: the burst rate limit (30 requests per 3 seconds for `/orchestrate`, 15 for `/bench`). A call made with an API key is counted against that key and against the key owner's total, wherever it is called from; a signed-in session is counted per user, and a call with no credential per IP. `API_KEY_FAILURE_RATE_LIMITED`: too many calls with an invalid or refused API key from this network. `API_KEY_CONCURRENCY_LIMITED`: this API key already has 5 agent turns in flight across `/orchestrate` and `/bench`. Each carries a `Retry-After` header.
>

Response shape: `object`

### `502` Bad Gateway

Non-streaming only (`stream: false`): the turn produced no answer because of an upstream failure. The error `code` says why; a guest receives no internal detail.

Response shape: `object`

### `503` Service Unavailable

`GUEST_CAPACITY_REACHED`: the shared guest pool is busy. Retry after the `Retry-After` header.

Response shape: `object`

**curl**

```bash
curl -X POST 'https://ivybiosciences.com/api/v1/orchestrate' \
  -H "Authorization: Bearer $IVY_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "prompt": "Predict ADMET properties for aspirin"
}'
```

**JavaScript**

```js
const res = await fetch('https://ivybiosciences.com/api/v1/orchestrate', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.IVY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "prompt": "Predict ADMET properties for aspirin"
}),
});
const data = await res.json();
```

**Python**

```python
import os, requests

res = requests.post(
    "https://ivybiosciences.com/api/v1/orchestrate",
    headers={"Authorization": f"Bearer {os.environ['IVY_API_KEY']}"},
    json={
  "prompt": "Predict ADMET properties for aspirin"
},
)
data = res.json()
```
