This is the full developer documentation for AgentDepot Docs
# AgentDepot Documentation
> Guides and API reference for the AgentDepot REST API and MCP server.
Welcome to the AgentDepot developer documentation. AgentDepot is a platform for building and running AI agents connected to your organization’s resources through tools.
There are two ways to drive AgentDepot programmatically, and both are documented here:
## Guides
[Section titled “Guides”](#guides)
Start with the [Overview](/docs/guides/overview) to learn what the platform is, then read [Core concepts](/docs/guides/concepts) and [Setting up a full flow](/docs/guides/build-a-flow). These are the same guides the MCP server serves to connected agents via `read_documentation` — one source of truth.
## REST API reference
[Section titled “REST API reference”](#rest-api-reference)
The [REST API reference](/docs/reference/rest/) documents every public endpoint of the AgentDepot HTTP API, grouped by resource. It is generated from the live OpenAPI schema.
All requests go to the base URL **`https://api.agentdepot.org`**, followed by the endpoint path exactly as documented — for example `https://api.agentdepot.org/api/orgs/{org_id}/skills`.
## MCP server reference
[Section titled “MCP server reference”](#mcp-server-reference)
The [MCP server reference](/docs/reference/mcp/) documents every tool the AgentDepot MCP server exposes (usable from Claude Desktop, Cowork, and other MCP hosts), grouped by category. It is generated from the live tool catalog.
## For agents
[Section titled “For agents”](#for-agents)
Every page on this site is also available as raw Markdown: append `.md` to any docs URL (for example, [`/docs/guides/overview.md`](/docs/guides/overview.md)). A machine-readable index lives at [`/llms.txt`](/llms.txt), and the full corpus at [`/llms-full.txt`](/llms-full.txt).
# Agent Environments
> Where an agent's files and commands run — our built-in sandbox, your own container image, or a machine you own.
An **environment** is where an agent’s files live and where its commands run. When an agent writes a file, runs a shell command, or executes a snippet of Python, that happens inside an environment. Every org has one by default and never has to think about this page.
You choose an environment per agent, and you can override it for a single chat from the composer. Environments are set up under **Settings → Environments**.
## The three kinds
[Section titled “The three kinds”](#the-three-kinds)
**Built-in sandbox.** Our own cloud container: a working directory, a terminal, Python and Node already installed, and internet access. Nothing to set up and nothing to run. This is what every agent uses unless you say otherwise.
**Your own container image.** A container image you build and publish, which we pull and run on our infrastructure. Use this when your agents need tooling that does not come as a quick install — a cloud CLI, a compiled binary, an internal command-line tool. You get your tools without running a machine.
**A machine you own.** Our small agent daemon installed on your own laptop, server or build box. The agent runs there, as you, with your permissions — nothing is sandboxed and nothing is blocked, but every command it runs is visible in the chat. Use this when the work has to happen somewhere we cannot reach: inside your network, against a checkout on your disk, with credentials that never leave your machine.
The rest of this page is about the second one.
## Building an image we will accept
[Section titled “Building an image we will accept”](#building-an-image-we-will-accept)
Start from our published base image and add your tooling. This is a complete, working example — it is how our own reference image with the Azure CLI is built:
```dockerfile
FROM ghcr.io/devdepot-ai/sandbox-base:1
USER root
RUN apk add --no-cache libffi openssl \
&& PIP_USER=0 pip install --no-cache-dir --break-system-packages azure-cli \
&& az version
USER node
```
Build it for Linux on x86-64 and push it somewhere public:
```bash
docker buildx build --platform linux/amd64 -t ghcr.io/acme/sandbox:v1 --push .
```
Then paste `ghcr.io/acme/sandbox:v1` into **Settings → Environments → New environment → Custom image**.
### The rules
[Section titled “The rules”](#the-rules)
* **Build `FROM ghcr.io/devdepot-ai/sandbox-base:1`.** This is the one requirement that is not negotiable. Our base image carries a marker that container images inherit automatically through `FROM`, and we check for it — so “it must build on our base” is something we can verify rather than merely ask for. An image without the marker is rejected with exactly that one-line fix. The tag is the *contract version*, not a release number: an image built on contract `1` keeps working when we publish contract `2`.
* **It must be public.** We pull with no credentials at all, so private registries are not supported yet. We pull from Docker Hub, GitHub Container Registry (`ghcr.io`), Google Artifact Registry (`gcr.io`, `pkg.dev`), Azure Container Registry (`azurecr.io`), Amazon ECR Public (`public.ecr.aws`) and Quay (`quay.io`) — subdomains included, so `myorg.azurecr.io` is fine. Always include the host: `acme/sandbox` on its own is refused rather than quietly assumed to mean Docker Hub.
* **It must be `linux/amd64`.** Every machine that runs sandboxes is x86-64. An image built on an Apple-silicon laptop defaults to arm64, starts perfectly well, and then fails every single command — so we reject it up front instead. `--platform linux/amd64` is the whole fix.
* **It must be 5 GB or smaller.** Your plan may allow less; it can never allow more. The ceiling is disk on the machines that run your agents, not policy. Very large images can also time out while being pulled even when they are under the limit.
### Two things that surprise people
[Section titled “Two things that surprise people”](#two-things-that-surprise-people)
**Your image runs as user 1000 with a read-only root filesystem.** Whatever `USER` your image declares, we start it as uid 1000 — so anything you install has to be readable and runnable by that user, and an image whose tooling only works as root is rejected during validation rather than failing mysteriously mid-conversation. Only `/workspace` (the agent’s working directory) and `/tmp` are writable at run time; everything else is read-only. Install what you need at build time and it will be there.
That read-only root filesystem is also why the example above says `PIP_USER=0`. Our base image configures `pip` to install into the user’s home directory, so that an agent can `pip install` something mid-conversation and have it work. That directory is temporary storage at run time — so a plain `pip install` in *your Dockerfile* appears to succeed at build time and then is not there when the agent starts. `PIP_USER=0` puts the packages in the system location, where they survive. Installing through your distribution’s package manager (`apk add`) is unaffected.
**The image has outbound internet, but no reach into private networks.** Downloading a file, calling a public API and talking to a cloud provider all work. Reaching a private address — something on our internal network, or on yours — does not, by design. If your agent needs to reach something private, that is what “a machine you own” is for.
## What happens after you paste the reference
[Section titled “What happens after you paste the reference”](#what-happens-after-you-paste-the-reference)
We pull the image, then run it through a few dozen checks: the base marker, the platform, that it starts and stays running as uid 1000, that the working directory is writable, that the root filesystem really is read-only, and that every file operation and command the agent will actually use — write, read, edit, search, shell, Python, snapshot — works end to end inside your image. It is the real machinery, not an approximation of it, so an image that passes here works in a real conversation.
Pulling and checking a large image takes minutes. The environment sits in **Validating** while it happens; you can leave the page.
**If it is rejected, the report names the fix.** Each failed check is written to say what breaks and what to do about it, not just what was missing — “missing `python3`” is not something you can act on, “file listing and artifact delivery break on every turn” is. Fix it, push again, and press **Update**.
Some findings are warnings rather than rejections. They appear on images that passed and are in use, and they mean something is degraded rather than broken — the most common one is that your image was built on an older version of our base image, which is a nudge to rebuild, never a reason we stop running it.
## We pin the image, so a new push changes nothing on its own
[Section titled “We pin the image, so a new push changes nothing on its own”](#we-pin-the-image-so-a-new-push-changes-nothing-on-its-own)
This is the one behaviour worth reading twice.
When validation succeeds, we record the exact image we checked and your agents run *that*, permanently. We do not follow your tag. If you rebuild and `docker push` to `ghcr.io/acme/sandbox:v1` again, your agents keep running the version we validated — the new one reaches nobody until you ask for it.
Asking for it is the **Update** button on the environment. It pulls the reference again, re-runs every check, and — if it passes — switches your agents to the new image. Until it passes, they run nothing: Update lets go of the current pin the moment you press it, because it is pointing at something we have not looked at yet.
This is deliberate. A tag is a moving pointer, and following one would mean the image under a running agent could change at any moment, without anybody deciding and without anything having checked the new version.
**Re-validate**, next to it, is the one that moves nothing. It re-runs the checks against the image we pinned — the exact bytes your agents are running — and never looks at your tag. Pressing it cannot pull in a push you have not asked for, and it keeps your current image and report in place until the new run finishes, so re-checking a working environment cannot leave you with nothing.
That makes it the button to press after we publish a new version of our base image: it tells you how the image you are *actually running* scores against today’s checks, which is what decides whether a rebuild is worth your time. If it is, Update is how you ship it.
One edge: an environment that failed before we ever got as far as pinning a digest has nothing to re-check. There, Re-validate pulls your reference again — the only thing it could do, and how you get a fresh verdict once you have fixed whatever went wrong.
## A broken environment fails, it does not fall back
[Section titled “A broken environment fails, it does not fall back”](#a-broken-environment-fails-it-does-not-fall-back)
If an environment cannot start — the image was rejected, or something goes wrong at run time — the agent’s turn fails with a message naming the environment, and we are alerted. It does **not** quietly run in the built-in sandbox instead.
That is on purpose. An agent that silently loses your tooling looks like it is working: it answers, it runs commands, and every command that needed your CLI fails for reasons nobody can explain for a week. A failed turn you can see beats a downgrade you cannot. So if your agent is behaving oddly, a silent switch back to our built-in sandbox is never the explanation.
## The agent is told what your image adds
[Section titled “The agent is told what your image adds”](#the-agent-is-told-what-your-image-adds)
An image is only useful if the agent knows to reach for what is in it. So when we validate your image, we work out what it carries that our base image does not — every command-line tool on the path, minus everything ours already has — and put that list in the agent’s own context, along with the Python packages your image has installed.
The practical effect: install `az` and an agent asked to check a subscription reaches for `az` on its own, instead of explaining that it has no way to talk to Azure. Without this, a tool can be installed, on the path, working, and still never used.
You can see the same list yourself on the environment, under the details we collected while validating it.
# Mention tokens in agent instructions
> Mention tokens (/type[key]) in agent instructions.
Agent instructions can embed **mention tokens** — short references to org resources — that the platform resolves at run time. The syntax is:
```plaintext
/type[key]
```
The resolved resource is appended to the agent’s system prompt as a “Referenced Resources” block that tells the agent exactly what the resource is and which tool to use to act on it. The agent sees the friendly description; you just embed the token.
## Supported types
[Section titled “Supported types”](#supported-types)
| Token | `key` value | What the agent is told to do |
| ----------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `/prompt[slug]` | Prompt template slug | Call `run_prompt` with `prompt=""` and the required variables (or material input, if the prompt has an extraction schema) |
| `/agent[slug]` | Agent slug | Start a work chat via `create_chat` with `agent_slug=""` (fire-and-forget) |
| `/skill[slug]` | Skill slug | Call `activate_skill` with `slug=""` to load its instructions |
| `/tool[name]` | Custom tool name | Call the tool directly by name |
| `/connected-tool[slug]` | Integration slug (or name) | Use the integration’s tools (available prefixed with `_`) |
A prompt with an extraction schema (`fields`, see `prompt-fields`) is still referenced with `/prompt[slug]` — there is no separate extractor token.
## Example
[Section titled “Example”](#example)
An instruction that classifies a ticket, drafts a reply, and optionally escalates:
```plaintext
When a new support ticket arrives:
1. Use /prompt[ticket-classifier] to extract the category and priority.
2. If priority is "urgent", hand it off to /agent[escalation-bot].
3. Otherwise, use /prompt[draft-reply] with variables {category} to draft a reply.
```
## Notes
[Section titled “Notes”](#notes)
* Keys are **slugs** (not display names) for prompts, skills, and agents. Use `list_prompts`, `list_skills`, or `list_agents` to find slugs.
* Custom tool keys are the **tool name** (not slug), matching the name it was created with.
* Unresolved mentions (typos, deleted resources) are handled gracefully — the agent is warned in-context that the resource was not found.
* Duplicate tokens are deduplicated; order of first appearance is preserved.
# Setting up a full flow
> Set up a full agent flow end-to-end with these tools.
A typical end-to-end setup using these tools:
1. **Pick the org** — if you belong to several, call `list_orgs` and pass `org=` to each tool (single-org accounts can omit it).
2. **(Optional) build reusable blocks first** so the agent can reference them:
* `create_code_tool(name, source_code)` for a custom Python tool. Give its entrypoint a normal docstring — first paragraph = the tool description, an `Args:` section = the parameter descriptions — or the model calling it has to guess what to pass.
* `create_prompt(name, text)` — pass `fields` too for a prompt that returns structured data instead of free text (see `prompt-fields`) — / `create_skill(name, instructions, tool_slugs)`.
3. **Create the agent** — `create_agent(name, instruction=..., provider=..., model=..., allowed_tools=[...])`. `allowed_tools` accepts tool scope strings or slugs.
4. **Wire / adjust it** — `update_agent(agent, instruction=..., allowed_tools=..., mounted_skills=[...])`. Partial: only the fields you pass change.
5. **Test before deploying** — try the draft without making it live:
* `start_chat(agent, message, use_draft=true)` runs the chat on the **unpublished draft** instruction. Poll `get_chat` as usual.
* Building blocks in isolation: `test_code_tool(tool, args)`, `test_prompt(prompt, variables)` (or `test_prompt(prompt, input_text=...)` for a schema-bearing prompt) — no chat needed. See the `testing` doc.
6. **Deploy** — `deploy_agent(agent)` to make the draft instruction active. (Skills/code tools/prompts don’t need a deploy step.)
7. **Run it** — `start_chat(agent, message)` → returns a `chat_id`.
8. **Read the result** — poll `get_chat(chat_id)` until `status` != “running”; read the final assistant message + `outcomes`. If it’s “paused” with a `pending_hitl_request`, answer via `respond_to_hitl`.
Use `list_*` / `get_*` tools at any point to inspect existing entities.
# Chat lifecycle & polling
> How chats execute asynchronously and how to poll them.
Chats execute **asynchronously** on a background worker — `start_chat` / `send_message` return immediately with `status: "accepted"`; the turn runs after.
`get_chat(chat_id)` is the poll target. Its `status`:
* **running** — a turn is in flight. Keep polling.
* **active** — idle / turn finished. Read the last assistant message + `outcomes`.
* **paused** — waiting on a human. `pending_hitl_request` describes what’s needed (`question`, `request_type`, `tool_name`/`tool_args`, and `ui` when the request offered specific options — e.g. `[{"buttons": ["Retry now", "Skip Todoist"]}]`). Resolve with `respond_to_hitl(request_id, action=... | text=... | selection=...)`, which resumes the turn. When `ui` offered `buttons`/`choice`, `action`/`selection` must be one of those options — anything else is rejected with the valid list. Sending a new `send_message` also resumes a paused chat.
* **errored** — the turn failed.
Note a tool-only turn may produce no new assistant message — rely on `status` and `outcomes`, not message presence. `outcomes` is where completed goals are recorded (status success / failed / partial + a `summary`).
# Use with Claude Desktop
> Connect Claude Desktop, claude.ai, or Claude Code to AgentDepot as a custom connector and manage your agents by chatting with Claude.
AgentDepot runs a hosted [MCP](https://modelcontextprotocol.io) server that lets Claude manage your AgentDepot workspace directly from a conversation: create and configure agents, build reusable blocks (code tools, prompts, skills), start chats, and read their results.
The server URL is:
```plaintext
https://api.agentdepot.org/mcp
```
Authentication happens through your AgentDepot account via OAuth — no API keys to copy around.
## What you need
[Section titled “What you need”](#what-you-need)
* A **paid Claude plan** (Pro, Max, Team, or Enterprise). Custom connectors are not available on the free plan.
* An **AgentDepot account** that belongs to at least one organization.
## Connect on claude.ai
[Section titled “Connect on claude.ai”](#connect-on-claudeai)
1. Open [claude.ai connector settings](https://claude.ai/new?modal=add-custom-connector#settings/customize-connectors) — this link opens the connector settings directly. (Manual path: **Customize → Connectors → Add custom connector**. On Team/Enterprise plans, connectors are managed under **Organization settings** by an admin.)
2. Enter a name (e.g. `AgentDepot`) and the server URL `https://api.agentdepot.org/mcp`. Leave the advanced OAuth fields empty.
3. Click **Add**, then **Connect**. A window opens asking you to sign in to AgentDepot and approve access.
4. After approving, the connector shows as connected.
## Connect in Claude Desktop
[Section titled “Connect in Claude Desktop”](#connect-in-claude-desktop)
Connectors are tied to your Claude account, so anything you add on claude.ai is also available in Claude Desktop after a restart. To add it from the app instead: **Settings → Connectors → Add custom connector**, then follow the same steps as above.
## Connect in Claude Code
[Section titled “Connect in Claude Code”](#connect-in-claude-code)
```plaintext
claude mcp add --transport http agentdepot https://api.agentdepot.org/mcp
```
Claude Code opens your browser for the same OAuth sign-in on first use.
## Using the connector
[Section titled “Using the connector”](#using-the-connector)
* In a chat, open the **+** (tools) menu, choose **Connectors**, and make sure AgentDepot is enabled for the conversation.
* Ask Claude to *“read the AgentDepot documentation”* — the connector ships a `read_documentation` tool that teaches Claude the platform end to end, so you can immediately ask for things like *“create an agent that triages incoming support email”*.
* If you belong to several organizations, tell Claude which one to work in, or ask it to *“list my orgs”*.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
* **“Your account was authorized, but AgentDepot returned an error when connecting.”** Try connecting again. If it keeps failing, remove the connector and add it back.
* **Claude doesn’t see any AgentDepot tools.** Check that the connector is enabled for the current conversation via the **+** menu.
* **Requests start failing after working fine.** Your session may have expired — open the connector’s settings and reconnect.
# Core concepts
> Core concepts: agents, revisions, building blocks, chats, outcomes, and HITL.
* **Agent** — the primary entity. Has a `name`/`slug`, an LLM (`provider`/`model`), an **instruction** (its task definition), an allowed toolset, and mounted skills. `execution_mode` is auto / manual / paused / dry\_run (a dry run starts chats but suppresses write/execute tools, frozen per chat at creation); `status` is active / archived.
* **Effort** — how an org whose plan selects *effort* rather than models picks its LLM: set `model_mode` (`trivial` | `normal` | `high_effort` | `x_high`, shown as Trivial / Standard / High / X-High) on the agent instead of `provider`/`model`, and the platform decides what that level runs. Trivial is the cheap rung for routine work and burns a fraction of Standard; High and X-High buy dearer models for harder work. No level is plan-gated. `start_chat` takes a `model_mode_override` to change one chat’s level in either direction (sticky for that chat’s later messages). Exactly one of the two selections is meaningful per plan — an org with direct model choice is refused a level rather than storing an inert one, and the refusal names the levels on offer.
* **Revisions & drafts** — a revision is an agent’s complete behavioural configuration: instruction, model, reasoning effort, mode, allowed tools, knowledge bases, mounted skills, pins, outcome schema, and the capability flags. Editing ANY of those with `update_agent` lands on a **draft** revision (forked from the active one) — none of it is live until you `deploy_agent` or pass `deploy=true` to `update_agent`/`create_agent`. Fields outside that list (name, description, status, execution\_mode, tags, per-chat caps) apply immediately either way. `get_agent` shows `instruction`/`active_config` (active) plus `draft_instruction`/`draft_config`/`has_undeployed_draft`/ `draft_changed_fields` (what deploying now would change).
* **Building blocks** (reusable, org-scoped) the agent composes:
* **Code tool** (`code_tool`) — a custom Python tool: one public entrypoint function plus any `_`-prefixed helpers. What the model sees comes from a normal docstring on that function (the module docstring is the fallback) — its **first paragraph** is the tool description and an **`Args:` section** describes the parameters — plus the **type annotations** for the parameter types. A parameter can carry its own description with `Annotated[str, "..."]` instead, which wins over `Args:`. A YAML `manifest:` block in the docstring is a deprecated legacy form; write `Args:`.
* **Prompt** — a reusable prompt template with `{{variables}}`. Optionally carries an extraction schema (`fields`, see `prompt-fields`) — when set, running it pulls structured fields out of input instead of generating free text.
* **Skill** — a bundle of {instructions + a tool subset + reference material} an agent mounts by slug. Live immediately on create/update.
* **Integration** — a connection to an external SaaS / MCP server (read-only here; connect new ones in the web UI — many need an interactive OAuth flow).
* **Environment** — where the agent’s files live and its commands run. Defaults to our built-in cloud sandbox; an org can instead point an agent at its own container image or at a machine it owns. See `agent-environments`.
* **Chat** — the unit of work. You `start_chat` with a message; the agent runs a turn. Success is recorded as **outcomes** (not a terminal status).
* **Outcome** — an append-only record the agent emits when it completes a goal (status success / failed / partial, with a summary).
* **HITL** — human-in-the-loop. A chat can pause awaiting approval or input; you answer with `respond_to_hitl` to resume it.
# Files
> Read, list, and search files attached to or produced in chats.
A **file** is anything attached to or produced inside a chat. Two things look identical here on purpose — you do not need to tell them apart:
* **uploads** — files a user attached to the chat.
* **artifacts** — outputs an agent produced (reports, code, charts, stamped PDFs).
All three file tools speak one vocabulary; each returned file carries a `kind` (`upload` or `artifact`) if you ever need to know.
* `read_file(file_id)` — full text content of one file by id (upload OR artifact). Images / scanned files return metadata + a note (vision reads aren’t available over MCP).
* `list_files(chat_id)` — everything in a chat: uploads + artifacts produced there.
* `search_files(...)` — find files across scopes: by `chat_id`, by `agent`, or org-wide, optionally filtered by `title_contains` / `type`.
This is the same surface agents use at run time, so what you see here matches what an agent sees.
# Inboxes
> Configure intake inboxes that turn inbound email and webhooks into agent chats and process runs.
An **inbox** is an org-owned address — an email address, a webhook URL, or both — that accepts inbound traffic from outside the platform, evaluates it against a versioned rule set, and routes what survives to an **agent** (as a new chat) or a **process** (as a new run). It is the front door for work that starts outside AgentDepot: a customer emailing a support address, a SaaS firing a webhook, a form submission.
Everything that arrives is logged, whatever happens to it next. Nothing here runs on its own — an inbox with channels but no deployed revision quarantines every message it receives, because no route ever matches.
## The four-stage pipeline
[Section titled “The four-stage pipeline”](#the-four-stage-pipeline)
Every rule belongs to exactly one stage, and stage order is **fixed and structural** — it is a property of the rule kind, not something you can reorder:
```plaintext
transform -> gate -> enrich -> route
```
* **transform** mutates the message (e.g. stripping attachments) and can never reject it.
* **gate** looks at the message and returns a verdict: continue, drop, or quarantine. Free gates always run before the paid one, so the classifier never has to look at what a free rule would already have dropped.
* **enrich** annotates the message without judging it — a failed enrichment never blocks anything, because enrich has no verdict to return.
* **route** decides where an accepted message goes; the first match wins, with an optional default that always sorts last.
Within one stage, a rule’s `order` field controls its position relative to other rules in that *same* stage — that is the only ordering you control. You cannot move a rule to a different stage, and you cannot make a `route` rule run before a `gate` rule. This is deliberate: it is what stops a paid classifier from being dragged in front of a free header check, and what stops a route from claiming a message before the gates had a chance to reject it.
## The three verdicts
[Section titled “The three verdicts”](#the-three-verdicts)
Every message gets exactly one of:
* **`accepted`** — passed every gate and matched a route; a chat or process run was created for it.
* **`dropped`** — a gate rejected it with high confidence (e.g. it matched a sender denylist). **Never bounced** — a bounce to an autoresponder is exactly what creates a mail loop, so a drop is silent by design.
* **`quarantined`** — something was uncertain (a classifier timed out, a provider errored, the message hit a rate limit, or a gate’s action was set to quarantine instead of drop) and a human needs to look at it.
There is no fourth verdict, and dropped mail is never retried automatically.
### What an accepted email leaves on the chat
[Section titled “What an accepted email leaves on the chat”](#what-an-accepted-email-leaves-on-the-chat)
The routed agent’s chat gets the message body as its first user turn, every attachment as a chat file, and one more file holding the **original email**: `email.html` when the message had an HTML part, `email.txt` otherwise. That file is stored exactly as it arrived, with an RFC822 header block prepended — `From`, `To`, `Subject`, `Date`, `Message-ID`, `In-Reply-To`, `References`.
Reach it with `list_files` / `read_file`. It is where to look when the agent needs the layout, tables or links of the mail the sender actually composed, or its `Message-ID` — e.g. to derive a deterministic id for whatever the message creates downstream, so a re-sent email cannot produce a second copy. A webhook message has no such file.
### While it is still being decided
[Section titled “While it is still being decided”](#while-it-is-still-being-decided)
A message shows up in the log the moment it arrives, before the rules have run — gates and routing can spend tens of seconds in model calls, and a log that only showed finished work made that whole window look like nothing had happened. Those rows carry `evaluation_state: "evaluating"`, and their `verdict` reads `quarantined` because nothing has been admitted yet.
That is a state, not a verdict, which is why it is a separate field. Two things follow when you read the log:
* Filtering `verdict="quarantined"` returns **settled** messages only — the ones actually held for a human. In-progress messages are reached with `evaluation_state="evaluating"` instead.
* If the platform dies mid-pipeline, the row stays `evaluating` and is genuinely held: it reports `is_stranded: true` once nothing is working on it any more, and running it again re-evaluates it against the live rules. A message that is merely still in flight refuses that, because evaluating one message twice at once could dispatch it twice.
## Rule kinds
[Section titled “Rule kinds”](#rule-kinds)
| Kind | Stage | Purpose |
| ------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `strip_attachments` | transform | Remove attachments matching mime/filename/size/disposition predicates (e.g. inline signature logos). |
| `header_gate` | gate (free) | Drop/quarantine on RFC 3834 auto-submitted / bulk-mail headers — the primary, zero-cost loop defence. |
| `phrase_denylist` | gate (free) | Drop/quarantine on a substring match in subject/body. |
| `sender_denylist` | gate (free) | Drop/quarantine by exact sender address or domain. |
| `loop_guard` | gate (free) | Last-resort circuit breaker on the `References` self-chain — backstop, not the primary loop defence. |
| `injection_screen` | gate | Platform-owned, auto-inserted, prompt-injection screen — see below. Not something you add; `push_inbox_revision` puts one in every definition automatically. |
| `semantic_gate` | gate (paid) | A yes/no question answered by a Prompt via a real classifier call; see below. |
| `prompt_extract` | enrich (paid) | Annotate the message with structured fields extracted by a Prompt. |
| `route_match` | route | Route to an agent or process if the message’s fields match a condition. |
| `route_semantic` | route (paid) | Route by meaning: a list of plain sentences, each with its own target, picked between by one model call. See below. |
| `route_default` | route | Catch-all route for anything no other route rule claimed. Always sorts last regardless of its `order`. |
This is the complete vocabulary — every rule config is validated with no extra fields allowed, so a typo in a field name is refused rather than silently ignored.
### `header_gate`’s two easy-to-get-wrong headers
[Section titled “header\_gate’s two easy-to-get-wrong headers”](#header_gates-two-easy-to-get-wrong-headers)
* `Auto-Submitted` is **not** a presence check. RFC 3834 defines `Auto-Submitted: no` as “a human sent this” — an explicit exemption, never a drop match. The default config already applies this exemption; if you write a custom `header_gate`, do the same, or you will drop mail from every sender scrupulous enough to set the header correctly.
* `Return-Path` only matches the literal empty `<>` form (a bounce) — never a presence check either.
### `injection_screen` — the platform-owned screen you don’t create, only toggle
[Section titled “injection\_screen — the platform-owned screen you don’t create, only toggle”](#injection_screen--the-platform-owned-screen-you-dont-create-only-toggle)
Every inbox is screened for prompt injection on every inbound message by default — the reason: intake is the platform’s untrusted-input door. Rather than a hidden setting, this is a real rule row (`injection_screen`) that `push_inbox_revision` inserts into a definition automatically if it isn’t already there, **enabled**. You can push a definition naming it explicitly with `"enabled": false` to switch screening off for that inbox; a later push that omits the rule entirely reinstates a fresh, enabled one rather than leaving the org silently unscreened. It cannot be reordered — its position in the gate stage (after every free deterministic gate, before any paid stage) is structural, not the rule’s `order` field. Disabling it costs nothing (no provider call), but it means inbound mail from unknown senders reaches the destination agent unscreened — the message-detail trace records that plainly (`outcome: "skipped"`, `detail.reason: "disabled"`) rather than silently.
### Writing a `semantic_gate`
[Section titled “Writing a semantic\_gate”](#writing-a-semantic_gate)
Set `question` to one line of text — “Is this a new work request?” — and that is the whole prompt. The JSON schema the model must answer in is generated around it, so there is no output schema to define, no `{{placeholder}}` to fill, and no separate object to create. The message itself is appended for you as a fixed rendering: channel, sender, recipient, subject, attachment *names* (never their content), then the body truncated to `body_chars`.
Two consequences of that rendering are worth designing around. **Attachments are names, mime types and sizes only** — “does this carry a schedule file?” is answerable, “does the attached PDF cover week 38?” is not. And **headers are absent**, because the free `header_gate` rules already ran on them before this point; asking a paid model to re-read them buys nothing.
`continue_when` decides which answer lets a message through, so one question serves both polarities: “is this a work request?” continues on `true`, “is this spam?” on `false`. `action` decides whether the other answer drops or quarantines.
Rules written before the question moved inline name a library prompt in `prompt_ref` instead. Those keep working, and `question` wins when a rule carries both — but do not point a *new* gate at a library prompt, and never at an extraction prompt: it validates clean, then arrives as thousands of characters of “question”, overruns the answer cap, and quarantines everything. Enrich (`prompt_extract`) is the opposite case and deliberately keeps the library, because an extraction prompt has a real output schema and is reused.
### Writing a `route_semantic`
[Section titled “Writing a route\_semantic”](#writing-a-route_semantic)
For traffic where the destination depends on what the message *means* rather than on any word it contains. Each branch is one plain sentence describing a kind of message, plus the agent or process that kind goes to:
```json
{
"id": "intents",
"name": "Accommodation intents",
"kind": "route_semantic",
"config": {
"branches": [
{"when": "This is a request to create a new accommodation",
"target": {"kind": "agent", "ref": "accommodation-create"}},
{"when": "This is a request to update an existing accommodation",
"target": {"kind": "agent", "ref": "accommodation-update"}},
{"when": "This is a request to cancel an accommodation",
"target": {"kind": "process", "ref": "accommodation-cancel"}}
]
}
}
```
All the branches go into **one** rule, and that rule costs **one** model call per message however many branches it holds. Do not split them across several `route_semantic` rules: you would pay per rule, and each call would see only its own sentence, so nothing could weigh “create” against “update” — which is usually the whole distinction you are trying to draw.
Four things to design around:
* **Write the branches to tell each other apart.** The discriminating word has to be in the sentence: “create a **new** accommodation” against “update an **existing** one”. Sentences that nest (“This is about accommodations”) swallow the ones beside them.
* **Order breaks ties.** The model returns one branch. If two could fit, it picks one and nothing arbitrates, so keep them mutually exclusive and put the narrower one first when they are not.
* **“None of these” is a normal answer, not an error.** It falls through to the next route rule, so give the inbox a `route_default` (or an inbox default handler) pointing at triage or a generalist. Without one, every message the branches do not describe is held for review.
* **The model never sees your agent names**, only the numbered sentences, and it answers with a number. Inbound mail asking to be sent to a particular agent therefore cannot route itself there.
Same message rendering as `semantic_gate` — attachment names but not their contents, no headers, body truncated to `body_chars` — and the same optional per-rule `model`.
### Choosing the model a paid rule runs on
[Section titled “Choosing the model a paid rule runs on”](#choosing-the-model-a-paid-rule-runs-on)
`semantic_gate`, `prompt_extract`, `route_semantic` and `injection_screen` each take an optional `model` — a catalog key from your own model list. Empty inherits the org’s `intake_classifier_default_model` (Usage & Limits → “Inbox Intake Classifier Model”), then the platform’s feature model. So a cheap deterministic gate and a subtle one can run on different models in the same inbox.
A key your catalog cannot serve is refused at validate and deploy time rather than at evaluation time. That is deliberate: an unroutable model raises nowhere a human is looking — it quarantines every message the rule reaches, which reads as a strict filter rather than a broken setting.
### `semantic_gate` quarantines when it cannot classify
[Section titled “semantic\_gate quarantines when it cannot classify”](#semantic_gate-quarantines-when-it-cannot-classify)
A `semantic_gate` rule calls a real classifier at evaluation time — it is not a stub, and it answers the rule’s question for real. It also never fails open: if the classifier is unbound, unroutable, times out, or the provider returns output in the wrong shape, the message is **quarantined**, never passed and never dropped, the same fail-closed default every other uncertain outcome in this pipeline gets. So a misconfigured or unroutable classifier shows up as *every* message reaching the gate being held for review — not as an error anywhere. If a `semantic_gate` rule is quarantining everything, check that the org’s intake classifier can actually run: either the org’s own model override (`Usage & Limits` → “Inbox Intake Classifier Model” in the app, or the `intake_classifier_default_model` org setting) or the platform’s own `intake_classifier` feature model needs to name a model the org can dispatch to.
## Channels: email and webhook, code vs. public
[Section titled “Channels: email and webhook, code vs. public”](#channels-email-and-webhook-code-vs-public)
An inbox may hold more than one channel of each kind.
* **Email** — an address of the form `in.{inbox}.{org}[.{code}]@...`. A **coded** channel (`has_code: true`) renders a random code as part of the address, so it is effectively private — nothing without the code can guess it. A **public** channel (`has_code: false`, the default) has a codeless, guessable address (derivable from the org and inbox slugs); pair a public inbox with a real `rate_limit_per_sender_per_hour` and deterministic gates, since anyone can find and mail it. Toggling coded ↔ public is a metadata flip, never a token regeneration — the address is never briefly unreachable.
* **Webhook** — an opaque URL a sender POSTs to, secured with an HMAC signature. The signing secret is never readable or rotatable through this MCP surface — that is a deliberate, UI-only, human-admin action. A webhook channel needs no payload configuration. **The whole inbound body reaches the agent** as the chat’s opening turn. How it reads depends on the payload’s shape, never on the `Content-Type`: a top-level `body` (or `text`) field holding a **string** leads the turn verbatim — Markdown arrives as the Markdown that was sent — and every other field follows it as JSON under an `Other fields from this webhook delivery:` trailer, except the keys read as `subject`/`sender` (already the chat’s title and the trigger header); any other shape renders whole as pretty JSON. Nothing is sampled out and nothing is dropped either way. A payload over \~2 KB, or one delivered with attachments, is additionally attached to the chat as a `payload.json` file the agent can `read_file`. Files posted alongside (`multipart/form-data`) become chat files, exactly as an email’s attachments do. Two fields are additionally read off conventional top-level names for display and for the gates — `sender` tries `sender`/`from`/`email`, and `subject` tries `subject`/`title`. Neither is required. This matters for gates: `sender_denylist` quarantines a message rather than passing it when the field it needs is unresolved, so a webhook whose payload carries no conventionally-named sender will pile up in quarantine rather than being silently filtered — check the message trace’s `not_applicable` outcome if that happens. A `phrase_denylist` gate reads the body, which is now the entire payload (prose lead plus trailer), so it matches on any value anywhere in it.
## Versioned revisions: draft, validate, deploy, rollback
[Section titled “Versioned revisions: draft, validate, deploy, rollback”](#versioned-revisions-draft-validate-deploy-rollback)
An inbox’s rule set is a **revision** — immutable once created, versioned, never renumbered:
1. **`push_inbox_revision`** — parse a definition against the rule vocabulary and save it as a new `draft`. Purely syntactic: a reference to an agent or prompt that doesn’t exist yet is not caught here, so you can push a draft while still building out the org resources it will target.
2. **`validate_inbox_revision`** — deep-validate a draft (or any revision): every `route_match`/`route_default` target, every `route_semantic` branch target, and every `semantic_gate`/`prompt_extract` `prompt_ref` must resolve in this org. Nothing is dispatched or mutated.
3. **`deploy_inbox_revision`** — re-runs the same validation and, on success, makes the revision LIVE immediately: the inbox’s current revision swaps atomically and the previously-deployed revision is archived. A message already mid-evaluation when this runs finishes on the revision it started with — nothing changes underneath an in-flight message.
4. **`rollback_inbox_revision`** — restores a previously-deployed (`archived`) revision as current. This is NOT a re-deploy: no new version number is created, and the revision is not re-validated (it was validated the first time it went live).
Revision status is one of `draft` / `deployed` / `archived`. Inspect the history with `list_inbox_revisions`, and one revision’s full definition with `get_inbox_revision`.
## Messages: the log
[Section titled “Messages: the log”](#messages-the-log)
Every message that reaches an inbox is logged — `list_inbox_messages` (filterable by verdict, channel, and a sender/subject substring search) and `get_inbox_message` (adds the per-stage evaluation trace, in the order the evaluator actually ran it).
A row carries **two** cost figures, and they answer different questions. `cost_credits` is the intake gate calls alone — a **per-message estimate**, never a ledger figure, and never to be summed across messages. `total_credits` (or `total_cost_usd`, if the org bills its own provider keys — one key is returned, never both) is what the message cost in **total**: intake plus the chat it dispatched, that chat’s descendants, and any process runs they invoked. A question about what a message cost means the total: `cost_credits` reads `0.0` on plenty of messages whose agents burned real money. `total_spend_chat_count` says how many chats it covers, and `total_spend_truncated` marks the figure as a floor when the lineage walk hit its cap. For an exact org-wide total use the credit ledger, which nets the grants and expiries these per-message figures cannot see.
## Replies: answering the sender
[Section titled “Replies: answering the sender”](#replies-answering-the-sender)
A revision can carry a `reply` block beside its `rules`, and it is how an inbox talks back:
```json
{
"version": 1,
"rules": [ ... ],
"reply": {
"triggers": [
{"id": "ack", "on": "accepted", "body": "Thanks — we're on {{subject}}."},
{"id": "done", "on": "work_done", "body": "Finished:\n\n{{summary}}"}
]
}
}
```
Six events, on two clocks. `accepted`, `dropped` and `quarantined` are decided by the rules, so a reply to one goes out as soon as the message is decided. `work_done`, `work_failed` and `awaiting_human` describe what the agent or process the message was routed to actually did, so they go out when that work stops moving — which for an agent that started other agents means once the whole chain has finished, without anything having to wait or be polled.
Pick the events you want; that *is* the timing choice. Acknowledge on `accepted` and answer on `work_done` is the usual pair.
* `to`: `"sender"` (default) answers whoever wrote in; `"fixed"` with an `address` sends somewhere else, and is the only mode that works for a webhook message, which has no sender. Deploy validation refuses the impossible combination rather than letting it silently never fire.
* `body` is markdown with `{{subject}}`, `{{sender}}`, `{{inbox_name}}`, `{{verdict}}`, `{{rule}}`, `{{message_url}}`, `{{summary}}` (work clock) and `{{chat_url}}`. Nothing is written by a model — the body is your text.
* One enabled trigger per event.
Some replies are refused whatever the policy says, and the message records why: mail that identifies itself as automated (`Auto-Submitted`, `Precedence: bulk`, `List-Id`), a bounce, our own address, a message the loop guard or a throttle stopped, a chat the agent already answered, an event already replied to, a re-run, and anything past the hourly per-recipient ceiling. Answering rejected mail is how mail loops are built, so a `dropped` trigger is available but never on by default.
### `notify_on_drop`: which rejections are worth answering
[Section titled “notify\_on\_drop: which rejections are worth answering”](#notify_on_drop-which-rejections-are-worth-answering)
The `dropped` trigger is one message for every rule that rejects, and the rules do not all deserve the same answer: “this is not a work request” is worth telling somebody, and “your bulk mail matched `no-reply@`” is worth telling nobody. Every rule takes a `notify_on_drop`, read only when that rule is the one that decided the message:
| value | what the rule’s drops do |
| --------- | ----------------------------------------------------------------------------- |
| `default` | the reply policy alone decides (the value every rule has until you change it) |
| `never` | this rule’s drops answer nobody, and the message log says `rule_opted_out` |
| `always` | answer even a sender the bulk headers call automated |
```json
{"id": "no-replys", "name": "Skip No-Replys", "kind": "phrase_denylist",
"notify_on_drop": "never",
"config": {"phrases": ["no-reply@"], "action": "drop"}}
```
`always` exists for one shape: a **staffed mailbox that forwards**. Its forwards carry the headers of whatever they forward, so a colleague who sends you a vendor newsletter arrives looking like the newsletter, and the automated sender check — which is a proxy, not a fact — stays silent at the one person who is waiting to hear back. `always` waives that check and nothing else: a bounce, our own address, the loop guard, a throttle trip and the outbound ceiling are not configurable and no value here reaches them.
It applies to `dropped` only. A quarantine is held for a human, and its reply is about the holding rather than about the rule.
`get_inbox_message` reports `reply_state`, `reply_event`, `reply_to`, `reply_at` and the full `reply_log`.
## Where inbox configuration lives
[Section titled “Where inbox configuration lives”](#where-inbox-configuration-lives)
Configuring inboxes (creating one, wiring channels, pushing and deploying rule-set revisions) is an **org-admin / MCP-client action** — there is no in-chat tool that lets a running agent configure its own or another inbox. If you want an agent to react to what an inbox routes to it, that happens naturally once the inbox’s `route_match`/`route_default` targets that agent — the agent just receives a chat like any other; it does not need (and cannot get) inbox-configuration tools of its own.
# What AgentDepot is
> What AgentDepot is and what this MCP server lets you do.
AgentDepot is a platform for building and running **AI agents** connected to your organization’s resources through tools. You create an agent, give it an instruction and a toolset, then assign it work as a **chat**; the agent executes, can call tools and other agents, and pauses for human approval when needed.
It’s a horizontal business-automation platform — integrations are general business SaaS (email, drive, CRM, docs, support desks, etc.), not just developer tools.
This MCP server lets you do all of that from here: create and configure agents, build reusable blocks (code tools, prompts — optionally with an extraction schema, skills), connect them, then start chats and read the results — the whole flow, without the web UI.
See also: `concepts`, `build-a-flow`, `chat-lifecycle`, `prompt-fields`, `agent-mentions`.
# Processes
> Define, validate, test, and monitor step-by-step processes.
A **process** is a compiled, deterministic SOP the platform executes step by step. It is NOT an agent chat: there is no free-running loop, and a run is a standalone object you watch with the run tools below (not a conversation). Reach for a process when the same procedure must run the same way every time — repeatable, auditable, with LLM judgment confined to small scoped steps. Reach for an agent when the work is open-ended and conversational.
## Anatomy of a definition
[Section titled “Anatomy of a definition”](#anatomy-of-a-definition)
A definition is plain data (YAML or JSON) — no expressions, no templates:
* `process` — the process’s name (slug). Must match the process it’s saved to.
* `envelope_schema` — JSON Schema for the **envelope**, the run’s input contract.
* `steps` — executed in order. Each step has:
* `id` — unique, referenced by other steps’ wiring.
* `sop` — required prose: the SOP sentence(s) this step implements.
* `block` — exactly one block kind (below).
* `params` — plain JSON for the block. Values may be reference strings (`"$envelope.external_id"`, `"$steps.extract.count"`, `"$flags"`, `"$item"` inside a fan-out) — references are the only interpolation.
* `inputs` — wiring from prior outcomes: `"envelope"`, `""`, `"."`, `"$flags"`.
* `when` — optional gate: `{step: classify, equals: update}` or `{step: extract, has: shifts}`. Presence / enum equality only.
* `map_over` — optional fan-out: one child per item of the referenced list (e.g. `envelope.files`).
* `on_fail` — `stop` (fail the run — the default) or `flag` (record a flag and keep going).
* `outcome` — optional final message; `{step_id.field}` placeholders resolve from step outcomes: `outcome: {message: "Done — {create.url}"}`.
### Files in the envelope
[Section titled “Files in the envelope”](#files-in-the-envelope)
An envelope can carry **file ids** — declare them as plain `type: string` fields in `envelope_schema` and wire them into steps like any other value (`params: {schedule_file: "$envelope.schedule_file"}`). When the target is a code tool whose parameter is annotated `bytes` (schema `type: file`), the platform resolves the id and hands the tool the actual file content at execution time — the same injection agents rely on in chat. Any file uploaded to the organization works (chat uploads included); get an id from `list_files` / `search_files`, or upload one with `create_upload_url`. Ids from another organization do not resolve and fail the step.
For **agent** steps there are two ways to give the agent the file itself:
* `attachments` on the agent block (e.g. `attachments: ["$envelope.timereport_file"]`) attaches the resolved file(s) to the bounded turn exactly like a chat attachment: images (scans, photos) are shown to the agent inline so it can read them visually; other files are announced in the task and read via `read_file`. An attachment that does not resolve fails the step rather than letting the agent run blind. When the step runs as a named agent (so it has a mirror chat), the attached files also appear in that chat’s Files panel.
* `read_file` inside an agent step resolves any org file id (not just the step’s attachments), so an agent step handed a file id through wired inputs can always open it on demand.
## Block kinds
[Section titled “Block kinds”](#block-kinds)
* **action** — a built-in platform operation (deterministic code): `block: {action: core.create_idempotent}` with `params: {tool: create_draft, idempotency_key: $envelope.external_id, ...}`.
* **code\_tool** — one of your org’s custom code tools, called with wired args: `block: {code_tool: extract-shifts}`. Reference the tool by its slug (the hyphenated identifier from `list_code_tools`).
* **llm** — a single tool-less structured model call: `block: {llm: {instruction: "Classify the request...", output_schema: {...}}}`.
* **prompt** — same as `llm`, but the instruction text comes from an org prompt: `block: {prompt: {ref: classify-request, output_schema: {...}}}`. The step still owns `output_schema`.
* **agent** — a bounded agent turn, the only kind that can use tools: `block: {agent: {ref: generic, tools: [search_customer], instruction: "...", output_schema: {...}, max_turns: 8, attachments: ["$envelope.scan"]}}`. `ref: generic` is an anonymous worker: it gets only the step’s `tools:` list and the platform’s default process model. To delegate the step to one of YOUR agents instead, set `ref` to the agent’s name (from `list_agents`) — it then runs as itself, with its own model and its own configured tools (the step’s `tools:` list is not needed). Either way the step’s instruction + wired inputs form the task, the loop is capped by `max_turns`, and the agent delivers the result itself by calling a `complete_step` tool whose arguments must match the step’s `output_schema`. Optional `attachments` (file-id values or refs) attach files to the turn like chat attachments — images are shown to the agent inline (see “Files in the envelope”).
* **process** — run another process as a child step: `block: {process: other-process}`. The step’s `params` ARE the child’s envelope — its keys must match the child process’s `envelope_schema` directly (`params: {topic: "$envelope.topic"}`, NOT wrapped in an `envelope:` key).
### Tool references
[Section titled “Tool references”](#tool-references)
Agent steps’ `tools:` lists and action steps’ `params.tool` accept slugs from the **org tool catalog** — browse it with `list_org_tools(query=?)`. Tools from an integration (MCP server) are catalogued under a prefixed slug `{prefix}_{tool}`: an ERP server’s `create_draft` appears as `erp_create_draft`. Look the exact slug up rather than guessing — the validator rejects anything not in the catalog (and suggests close matches). Custom code tools are the exception: a `code_tool` block references the hyphenated slug from `list_code_tools`. In action params (`params.tool`, `params.tools.*`, `map_dispatch` `variants`) a custom code tool may be named by either that hyphenated slug or its catalog name (source `custom` in `list_org_tools`). Every tool reference is a plain slug string — never a block object.
## Actions catalog
[Section titled “Actions catalog”](#actions-catalog)
The `action` block kind runs one of these built-in `core.*` operations. Every tool call inside an action is schema-gated, retried on transient failures, and repaired by a bounded one-shot agent on payload mismatch — the happy path involves no model calls.
* `core.call_tool` — one deterministic tool call. `params: {tool, args}`.
* `core.create_idempotent` — create through a tool; the `idempotency_key` (usually an envelope ref) guarantees a re-run never duplicates. `capture` lifts result fields onto the step outcome; `protected` registers payload fields no later update may re-send. `params: {tool, payload, idempotency_key?, capture?, protected?}`.
* `core.update_complement` — follow-up update restricted to what the create couldn’t set: `from` is the create step’s outcome (ref), `echo_exact` copies fields from it verbatim, protected fields are refused. `params: {tool, from, payload?, echo_exact?, require?, args?}`.
* `core.verify_fields` — fetch and assert: `expect` (field → exact value), `require_not_null` (fields present and non-null), `count` (`{field, equals}`), optional single-shot `repair: {tool, args}` then re-assert. `params: {tool, args?, expect?, require_not_null?, count?, repair?}`.
* `core.upload_attach` — ordered sequence create URL → push bytes → attach; the byte push between the two tool calls is automatic, with fresh-URL retry. `params: {tools: {create_url, attach}, file, create_url_args?, attach_args?, url_field?, upload_ref_field?, retry_fresh_url?}` — `file` is a ref to an item with `file_id` and `name`.
* `core.replace_collection` — delete-all + one bulk add + count assertion. `params: {tools: {delete, add}, items, items_field?, args?, delete_args?, add_args?, expected_count?, count_field?}`.
* `core.map_dispatch` — dispatch each item to a tool picked by a discriminator field; the item itself is passed under `args_field` automatically; unknown discriminator values flag the item. `params: {items, discriminator, variants, args_field?, args?}`. Each `variants` value is a plain tool-slug STRING (`"docx": "extract-shifts"`), never a block object like `{code_tool: …}`.
Validation errors report each action’s exact params schema when a step’s params don’t match — `validate_process_definition` is the authoritative reference for the current contracts.
## Lifecycle: draft, test, deploy
[Section titled “Lifecycle: draft, test, deploy”](#lifecycle-draft-test-deploy)
1. `create_process(name, title, description=?)` — creates the empty shell. `name` is the permanent lowercase-hyphen slug; `title` is the display name. No definition exists yet.
2. Write the definition and dry-run it with `validate_process_definition(definition)` — returns `{ok, errors, warnings}`. Fix errors and re-run until `ok` is true.
3. `update_process_definition(process, definition)` — saves it as a new **draft** revision (returns its version). Never deploys.
4. `test_process_run(process, envelope, revision_version=?)` — runs a revision end-to-end; pass the draft’s version to test it before anyone deploys.
5. `deploy_process_revision(process, version=? | revision_id=?)` — makes that revision LIVE: it is re-validated against the current catalog, the previously deployed revision is archived, and every future run uses the new one immediately. Always test the draft first. (The same deploy is available from the CLI as `agentdepot process deploy`. The web app’s Processes pages are read-only — there is no deploy button there.)
A process is not live until a revision is deployed: a freshly created shell, or a definition that was drafted and tested but never deployed, never runs. Deploying is the step that turns your work on.
Revisions have status draft / deployed / archived. Inspect them with `list_process_revisions(process)` and `get_process_revision(process, version=N)`; `get_process(process)` returns the currently deployed definition, and `list_processes()` is the org-wide starting point.
## Runs
[Section titled “Runs”](#runs)
A run’s status is `running`, `completed`, `flagged` (finished, but steps raised flags worth a look), `failed`, or `cancelled`. To watch one:
* `list_process_runs(process=?, status=?)` — recent runs, newest first.
* `get_process_run(run_id)` — the full picture: every step with its status, timing, error and outcome, plus the run’s flags, trigger envelope, and `revision_version`.
* `get_process_run_calls(run_id, step_id=?, include_messages=?)` — the model calls behind the steps, merged into one timeline; set `include_messages=true` to see the actual (truncated) request/response.
* `restart_process_run(run_id)` — re-runs a **finished** run’s envelope as a brand-new run on the current deployed revision.
* `cancel_process_run(run_id, reason=?)` — stops a run that is still running.
## Troubleshooting a run
[Section titled “Troubleshooting a run”](#troubleshooting-a-run)
1. `get_process_run` — find the first failed step; read its `error` and the outcomes of the steps before it.
2. `get_process_run_calls` scoped to that `step_id` with `include_messages=true` — see exactly what the model was asked and answered. `repair` entries mean a tool call’s payload failed validation; their verdict shows whether the automatic fix was accepted.
3. Check the run’s **flags** — non-blocking issues steps recorded while proceeding (`on_fail: flag`); they explain a `flagged` run.
4. Fetch exactly what ran: `get_process_revision(process, version=)` — the live definition may have moved on since.
5. Fix the definition → `update_process_definition` (new draft) → `test_process_run` pinned to the draft → `deploy_process_revision` to make the fix live.
# Structured output fields
> Field definitions for giving a prompt an extraction schema.
`create_prompt` / `update_prompt` take an optional `fields`: a list of field defs. When `fields` is set, the prompt runs as **structured extraction** instead of free-text generation — `run_prompt` / `test_prompt` return typed data, and `text` (if given) becomes the extraction instructions. Each field is an object with at least `name` and `field_type`. Valid `field_type`:
* `text`, `date`, `datetime`, `yes_no`, `whole_number`, `money`
* `choice` — also requires a `choices` list of allowed values.
Example:
```plaintext
[
{"name": "invoice_total", "field_type": "money"},
{"name": "due_date", "field_type": "date"},
{"name": "priority", "field_type": "choice", "choices": ["low", "high"]}
]
```
Passing `fields` to `update_prompt` creates a new revision. `post_processing_instructions` (also on `create_prompt`/`update_prompt`) runs a second refinement call over the extracted data — useful for cleanup or cross-field derivation that needs the full first-pass result in context.
# Testing before you publish
> Test agents and building blocks before you publish them.
You can validate changes without making them live to your org.
**Run a chat on an unpublished agent revision.** Instruction edits land on a draft (see `concepts`). To try the draft end-to-end before `deploy_agent`:
* `start_chat(agent, message, use_draft=true)` — pins the chat to the agent’s current draft revision.
* `start_chat(agent, message, revision_version=N)` — pins a specific past version (re-test an old instruction).
* Omit both → the chat runs the published (active) revision (the default).
The pin holds for the whole chat. `get_chat` reports `revision_version` (and `pinned_revision_id`) so you can confirm which revision ran — null means it followed the published one. In the web UI these chats show a “Running draft v{n}” badge. Iterate on the draft (`update_agent`) → test → `deploy_agent` when happy.
**Test building blocks in isolation** (no chat, no agent wiring):
* `test_code_tool(tool, args)` — runs the code tool’s current source in a sandbox and returns its `status`, `result`, `logs`, and any `error`.
* `test_prompt(prompt, variables, revision_version=?)` — renders the template and runs it against its LLM; returns `status`, `result` (the model output), `error`, `duration_ms`, and token `usage`. If the tested revision has an extraction schema (`fields`, see `prompt-fields`), pass `input_text` or `input_url` instead of (or alongside) `variables` — `result` is then the structured data. Pass `model`/`provider` to test with a specific model, or `model_mode` (`trivial` | `normal` | `high_effort` | `x_high`) for an org whose plan sells effort levels instead (see `concepts` for **Effort**) — whichever one your org’s plan doesn’t sell is refused with `422`. Omit both to run the template’s own stored model (or the platform default).
Each of these records a run, so it shows up on the building block’s Runs page. Prompts are revision-versioned: pass `revision_version` to test an unpublished revision; omit it for the latest. (Code tools have no revisions — the test runs the single saved source.) These all require an admin/owner role.
To test a **skill**, mount it on an agent and run a draft-revision chat — there is no isolated skill runner.
# MCP Server Reference
> Tools exposed by the AgentDepot MCP server.
The AgentDepot MCP server (mounted at `/mcp`) exposes the following tools, usable from any MCP host (Claude Desktop, Cowork, etc.). Authenticate with an `agd_*` API token or the OAuth flow.
* [Agents](/docs/reference/mcp/agents) — 11 tools
* [Building Blocks](/docs/reference/mcp/building-blocks) — 15 tools
* [Chats](/docs/reference/mcp/chats) — 5 tools
* [Documentation](/docs/reference/mcp/documentation) — 1 tool
* [Files](/docs/reference/mcp/files) — 4 tools
* [Integrations](/docs/reference/mcp/integrations) — 3 tools
* [Organizations](/docs/reference/mcp/orgs) — 1 tool
* [Skills](/docs/reference/mcp/skills) — 2 tools
* [Teams](/docs/reference/mcp/teams) — 6 tools
* [Testing](/docs/reference/mcp/testing) — 2 tools
## Resources
[Section titled “Resources”](#resources)
The server also exposes these read-only MCP resources:
| URI | Name | Description |
| -------------------------------------- | ------------------------------------ | ------------------------------------------------------ |
| `agentdepot://docs` | AgentDepot documentation index | Index of available AgentDepot documentation topics. |
| `agentdepot://docs/agent-environments` | Agent Environments | AgentDepot docs: Agent Environments. |
| `agentdepot://docs/agent-mentions` | Mention tokens in agent instructions | AgentDepot docs: Mention tokens in agent instructions. |
| `agentdepot://docs/build-a-flow` | Setting up a full flow | AgentDepot docs: Setting up a full flow. |
| `agentdepot://docs/chat-lifecycle` | Chat lifecycle & polling | AgentDepot docs: Chat lifecycle & polling. |
| `agentdepot://docs/claude-desktop` | Use with Claude Desktop | AgentDepot docs: Use with Claude Desktop. |
| `agentdepot://docs/concepts` | Core concepts | AgentDepot docs: Core concepts. |
| `agentdepot://docs/files` | Files | AgentDepot docs: Files. |
| `agentdepot://docs/intake` | Inboxes | AgentDepot docs: Inboxes. |
| `agentdepot://docs/overview` | What AgentDepot is | AgentDepot docs: What AgentDepot is. |
| `agentdepot://docs/processes` | Processes | AgentDepot docs: Processes. |
| `agentdepot://docs/prompt-fields` | Structured output fields | AgentDepot docs: Structured output fields. |
| `agentdepot://docs/testing` | Testing before you publish | AgentDepot docs: Testing before you publish. |
# Agents
> MCP server tools for agents.
### `create_agent`
[Section titled “create\_agent”](#create_agent)
Create a new agent.
Every behaviour field this tool accepts — `instruction`, `model`, `reasoning_effort`, `model_mode`, `vision_model`, `allowed_tools`, `allowed_knowledge_bases`, `outcome_schema` — is captured on the new agent’s v1 revision, which starts as a **draft**: it cannot run a chat until deployed. Pass `deploy=true` to publish v1 immediately, or call `deploy_agent` afterwards. `allowed_tools` is a list of tool scope strings or slugs; `model` picks the LLM. `tags` is a list of tag names to attach to the agent (created if they don’t exist yet).
**Per-chat spend cap:** the cap is denominated in the unit your org is billed in, and only that one is settable. An org billed in **credits** (every plan that does not bring its own provider keys) sets `per_chat_credit_limit` — the same unit `get_chat` reports spend in as `total_credits`, so the two are directly comparable. An org that pays its model providers directly sets `per_chat_cost_limit_usd`. Setting the other one is refused rather than stored, because credits are not a flat rate per dollar — the per-model multiplier means the same dollar cap buys a different amount of work on every model. `get_agent` reports whichever of the two applies, and never both. Omit both for the platform default.
**Effort vs models:** an org whose plan selects *effort* rather than models does not name a model — it sets `model_mode` (‘trivial’ | ‘normal’ | ‘high\_effort’ | ‘x\_high’, shown as Trivial / Standard / High / X-High) and the platform decides what that level runs (model + reasoning effort + thinking). No level is plan-gated. Exactly one of the two selections is meaningful per plan, so setting a level on an org with direct model choice is refused rather than stored inert; the refusal names the levels on offer.
**Mention tokens:** Instructions can reference org resources inline using `/type[key]` syntax (e.g. `/prompt[summarize]`, `/agent[researcher]`). The platform resolves them at run time and appends a “Referenced Resources” block to the agent’s system prompt. Read the `agent-mentions` documentation topic for the full list of supported types and their keys.
`decision_log` (‘on’ | ‘off’ | ‘inherit’) overrides the org’s decision log setting for this agent: ‘on’ makes it record its judgement calls with the `decide` tool whatever the org says, ‘off’ exempts it, ‘inherit’ (the default) follows the org. It is a behavioural setting, so it lands on the draft like the others.
`vision_model` overrides which model this agent uses to describe images and scanned pages it opens with `read_file`. Omit to defer to the org’s own override (if any), then the platform’s. Allowed values are the same models `model` accepts, narrowed to those whose catalog row supports vision; a model that cannot read images is refused.
Every `allowed_tools` entry must resolve in this org — a scope grant, or a tool slug from `list_org_tools` / `list_code_tools`.
`allowed_knowledge_bases` is the agent’s Knowledge Access: which knowledge bases it may search with `search_knowledge`. Pass base ids or slugs (`list_knowledge_bases`), `["*"]` for every base (the default when omitted), or `[]` for none — an agent granted none does not get the search tool at all.
**Outcome contract:** `outcome_schema` is a raw JSON Schema (draft 2020-12, root `type: object`, no `$ref`) that the agent’s structured result must satisfy. When set, every `success`/`partial` outcome the agent records is validated against it and the agent is handed the errors to correct; after five invalid attempts in one run the platform stops asking and records the outcome as failed. Set it when a system reads the agent’s result as fields rather than prose. Pass `{}` to remove an existing contract.
| Parameter | Type | Required | Description |
| --------------------------- | ---------- | -------- | ----------- |
| allowed\_knowledge\_bases | `string[]` | no | |
| allowed\_tools | `string[]` | no | |
| decision\_log | `string` | no | |
| deploy | `boolean` | no | |
| description | `string` | no | |
| instruction | `string` | no | |
| model | `string` | no | |
| model\_mode | `string` | no | |
| name | `string` | yes | |
| org | `string` | no | |
| outcome\_schema | `object` | no | |
| per\_chat\_cost\_limit\_usd | `number` | no | |
| per\_chat\_credit\_limit | `number` | no | |
| reasoning\_effort | `string` | no | |
| tags | `string[]` | no | |
| vision\_model | `string` | no | |
### `create_agent_draft`
[Section titled “create\_agent\_draft”](#create_agent_draft)
Start (or reset) an agent’s draft from a chosen version’s full config — model, effort, mode, tools, knowledge bases, mounted skills, pins, outcome schema, every capability flag, and the instruction. Unlike copying instruction text by hand, this is the rollback recipe: `create_agent_draft(agent, from_version=N)` then `deploy_agent(agent)` puts the agent back to exactly how v`N` behaved.
`from_version` omitted copies the currently **active** revision instead — useful to blow away an in-progress draft and start clean from what is live. Raises if `from_version` does not name a live (non-deleted) revision on this agent, or, when omitted, if the agent has never been deployed.
If the agent already has a draft with unsaved changes of its own (its `draft_changed_fields` against what is live is non-empty), this is refused naming how many fields would be discarded — pass `replace=True` to overwrite it anyway. A draft that already matches what is live is always overwritten, `replace` or not.
Legal while an experiment is running — only `deploy_agent` is refused then, not this.
| Parameter | Type | Required | Description |
| ------------- | --------- | -------- | ----------- |
| agent | `string` | yes | |
| from\_version | `integer` | no | |
| org | `string` | no | |
| replace | `boolean` | no | |
### `deploy_agent`
[Section titled “deploy\_agent”](#deploy_agent)
Deploy an agent’s draft, making it the active revision — instruction, model, tools, skills, outcome schema and every other behaviour field the draft carries, all at once. Refused if the draft is identical to what is already live (nothing to deploy), has no draft at all, or an A/B experiment is currently running on this agent (stop or promote it first — see `stop_experiment` / `promote_experiment_arm`).
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| agent | `string` | yes | |
| org | `string` | no | |
### `get_agent`
[Section titled “get\_agent”](#get_agent)
Get full detail for one agent (by id or slug), including its active instruction and any undeployed draft.
`active_config` / `draft_config` are the complete configuration each of those revisions carries (instruction, model, effort, mode, tools, knowledge bases, skills, pins, outcome schema, capability flags), with a `*_config_hash` fingerprint: equal hashes mean identical behaviour. `draft_changed_fields` names exactly which of those fields the draft would change if deployed right now — `[]` when there is no draft, or the draft is identical to what is live.
`running_rollout` is the agent’s current rollout — a plain deploy (one arm) or a live A/B experiment (2-5 arms, `kind: "experiment"`). `null` for an agent that has never been deployed.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| agent | `string` | yes | |
| org | `string` | no | |
### `get_experiment`
[Section titled “get\_experiment”](#get_experiment)
Get one rollout’s arms (label, revision version, weight), live per-arm chat counts (`counted_chats` / `excluded_chats`), and a compact `results` summary (outcome rates, mean cost/tokens, tool-call and HITL rates, and which metrics differ significantly from control — see `get_experiment_results` for the same shape on its own).
Pass `rollout_id` to fetch a specific rollout (running or ended); omit it to get whichever rollout is currently running — a plain deploy (`kind: "deploy"`) or a live experiment (`kind: "experiment"`). Errors if the agent has never been deployed, or `rollout_id` does not belong to it.
| Parameter | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| agent | `string` | yes | |
| org | `string` | no | |
| rollout\_id | `string` | no | |
### `get_experiment_results`
[Section titled “get\_experiment\_results”](#get_experiment_results)
A compact per-arm results summary for one rollout: outcome rates (success/partial/failed/no\_outcome/errored), mean cost (in this org’s billing unit) and tokens, tool-call count and error rate per chat, HITL requests per chat, median duration, and which metrics differ significantly from the control arm (95% confidence intervals that don’t overlap — a heuristic, not a p-value).
`insufficient_data` on an arm means its `counted_chats` are below the rollout’s `min_sample` — every figure is still computed, just worth treating cautiously. Cached for about 45 seconds per rollout.
Pass `rollout_id` to fetch a specific rollout (running or ended); omit it to get whichever rollout is currently running. Errors if the agent has never been deployed, or `rollout_id` does not belong to it.
| Parameter | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| agent | `string` | yes | |
| org | `string` | no | |
| rollout\_id | `string` | no | |
### `list_agents`
[Section titled “list\_agents”](#list_agents)
List the agents in an organization.
Paged: the response carries `total`, `has_more` and `next_offset` — pass `next_offset` back as `offset` to walk the rest.
| Parameter | Type | Required | Description |
| --------- | --------- | -------- | ---------------------------------------------------------- |
| limit | `integer` | no | Max rows to return (1–200). |
| offset | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org | `string` | no | |
### `promote_experiment_arm`
[Section titled “promote\_experiment\_arm”](#promote_experiment_arm)
End the agent’s running experiment as promoted, and deploy the named arm (by label) at 100% traffic. `arm` must name one of the running experiment’s arms. Refused if no experiment is currently running on this agent.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| agent | `string` | yes | |
| arm | `string` | yes | |
| org | `string` | no | |
### `start_experiment`
[Section titled “start\_experiment”](#start_experiment)
Start a 2-5 arm A/B experiment on an agent, splitting traffic by weight across sealed revisions. Supersedes whatever rollout is currently running — a plain deploy is replaced automatically; another running experiment must be stopped or promoted first.
Requires a platform admin to have turned on the `agent_experiments_enabled` setting — refused (403-equivalent ToolError) otherwise.
Each entry of `arms` is a dict: `{"label": str, "weight_bp": int, "revision_version"|"revision_id"|"use_draft": ...}` — exactly one of `revision_version` (int), `revision_id` (uuid string), or `use_draft: true` (the agent’s current draft, sealed on start) per arm. `weight_bp` are basis points (10000 = 100%) and must sum to 10000 across all arms (2-5 of them). `control` must name one of the arm labels — its revision is what `stop_experiment` deploys.
Every arm’s model must pass this org’s model allowlist, and its `allowed_tools` / `mounted_skills` / `allowed_knowledge_bases` must resolve in the org’s catalog and this agent’s own reach — the same checks `update_agent` runs, applied per arm. No two arms may share a configuration unless `allow_identical` is set (an A/A test).
`unit` picks what assignment sticks to: `chat` (default, one chat = one coin flip), `thread` (a whole email/webhook thread stays on one arm), `user` (the creator does), `sender` (the normalised sender address does).
| Parameter | Type | Required | Description |
| ---------------- | ---------- | -------- | ----------- |
| agent | `string` | yes | |
| allow\_identical | `boolean` | no | |
| arms | `object[]` | yes | |
| control | `string` | yes | |
| hypothesis | `string` | no | |
| label | `string` | no | |
| org | `string` | no | |
| unit | `string` | no | |
### `stop_experiment`
[Section titled “stop\_experiment”](#stop_experiment)
End the agent’s running experiment as stopped, and deploy its control revision at 100% traffic. Refused if no experiment is currently running on this agent.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| agent | `string` | yes | |
| org | `string` | no | |
### `update_agent`
[Section titled “update\_agent”](#update_agent)
Update an agent (by id or slug). Partial: only the fields you pass are changed; omit the rest to leave them untouched.
**Every behaviour field lands on the draft, not live.** That is `instruction`, `model`, `reasoning_effort`, `model_mode`, `vision_model`, `allowed_tools`, `allowed_knowledge_bases`, `mounted_skills`, `outcome_schema` — all of it. A draft is forked from the currently active revision the first time one of these is set, and nothing above changes what a running chat does until you deploy it: pass `deploy=true` to publish this same edit immediately, or call `deploy_agent` afterwards (`get_agent` shows `has_undeployed_draft` and `draft_changed_fields`). Everything else — `name`, `description`, `execution_mode`, `status`, the per-chat spend cap, `tags` — applies immediately regardless of `deploy`. `execution_mode` is auto/manual/paused/dry\_run (`dry_run` starts chats normally but suppresses write/execute tools — reads, lifecycle tools and `decide` still run; it applies to chats started after the change); `status` is active/archived. `tags` replaces the agent’s current tag set (pass an empty list to clear all tags).
**Effort vs models:** `model_mode` (‘trivial’ | ‘normal’ | ‘high\_effort’ | ‘x\_high’, shown as Trivial / Standard / High / X-High) is what an org whose plan selects effort sets *instead of* `model` — the platform decides what each level runs. Setting one on an org with direct model choice is refused (it would be stored inert), and the refusal names the levels on offer. Pass an empty string to clear the level back to the platform default; omitting it leaves whatever is set alone.
`decision_log` (‘on’ | ‘off’ | ‘inherit’) overrides the org’s decision log setting for this agent: ‘on’ makes it record its judgement calls with the `decide` tool whatever the org says, ‘off’ exempts it, ‘inherit’ (the default) follows the org. It is a behavioural setting, so it lands on the draft like the others.
`vision_model` overrides which model this agent uses to describe images and scanned pages it opens with `read_file`, ahead of the org’s own override (if any) and the platform’s. Allowed values are the same models `model` accepts, narrowed to those whose catalog row supports vision. Pass an empty string to clear it back to the org/platform default; omitting it leaves whatever is set alone.
**Mention tokens:** Use `/type[key]` tokens in `instruction` to reference org resources inline (e.g. `/prompt[slug]`, `/skill[slug]`, `/agent[slug]`, `/tool[name]`, `/connected-tool[slug]`). The platform resolves them at run time. See the `agent-mentions` documentation topic for details.
To save tokens the response OMITS the full instruction texts (it returns their lengths + revision ids + `has_undeployed_draft`); call `get_agent` for the full text.
Every `allowed_tools` entry must resolve in this org (a scope grant, or a slug from `list_org_tools` / `list_code_tools`), and every `mounted_skills` entry must be an existing skill slug (`list_skills`).
Narrowing `allowed_tools` also unpins: any of the agent’s pinned tools the new grants no longer admit is dropped (a pin is loaded directly, ahead of discovery, and would otherwise survive the revocation).
`allowed_knowledge_bases` replaces the agent’s Knowledge Access grant: base ids or slugs (`list_knowledge_bases`), `["*"]` for every base, or `[]` to revoke all knowledge. Omit it to leave the grant untouched.
**Outcome contract:** `outcome_schema` is a raw JSON Schema (draft 2020-12, root `type: object`, no `$ref`) that the agent’s structured result must satisfy. When set, every `success`/`partial` outcome the agent records is validated against it and the agent is handed the errors to correct; after five invalid attempts in one run the platform stops asking and records the outcome as failed. Set it when a system reads the agent’s result as fields rather than prose. Pass `{}` to remove an existing contract.
**Per-chat spend cap:** settable in your org’s own billing unit only. An org billed in **credits** sets `per_chat_credit_limit` — the unit `get_chat` reports spend in as `total_credits`, so cap and spend are directly comparable. An org that pays its model providers directly sets `per_chat_cost_limit_usd`. The other one is refused rather than stored: credits are not a flat rate per dollar, so a dollar cap buys a different amount of work on every model. The response reports whichever applies, never both.
| Parameter | Type | Required | Description |
| --------------------------- | ---------- | -------- | ----------- |
| agent | `string` | yes | |
| allowed\_knowledge\_bases | `string[]` | no | |
| allowed\_tools | `string[]` | no | |
| decision\_log | `string` | no | |
| deploy | `boolean` | no | |
| description | `string` | no | |
| execution\_mode | `string` | no | |
| instruction | `string` | no | |
| model | `string` | no | |
| model\_mode | `string` | no | |
| mounted\_skills | `string[]` | no | |
| name | `string` | no | |
| org | `string` | no | |
| outcome\_schema | `object` | no | |
| per\_chat\_cost\_limit\_usd | `number` | no | |
| per\_chat\_credit\_limit | `number` | no | |
| reasoning\_effort | `string` | no | |
| status | `string` | no | |
| tags | `string[]` | no | |
| vision\_model | `string` | no | |
# Building Blocks
> MCP server tools for building blocks.
### `create_code_tool`
[Section titled “create\_code\_tool”](#create_code_tool)
Create a code tool from Python `source_code`: exactly one public function (the entrypoint) plus any `_`-prefixed helpers. A parse error is returned as an error. `tags` attaches tag names to the new tool.
**How the tool is described to the model — write a normal docstring.** Everything the model sees comes out of the entrypoint’s docstring (the module docstring is used when the function has none):
* its **first paragraph** becomes the tool description, and the paragraphs after it are kept as the tool’s usage notes;
* an **`Args:` section** describes the parameters — write one line per parameter or the model is left guessing what to pass;
* parameter **types come from the annotations**, never from prose.
def run(upload\_url: str, mime: str, source\_file: bytes) -> dict: """PUT a file’s bytes to a presigned upload URL.
```plaintext
Args:
upload_url: The presigned uploadUrl from `create_upload_url`.
mime: Content-Type to send — the mimeType `create_upload_url`
returned.
source_file: file_id of the file whose bytes to PUT.
"""
```
Alternatively, keep the description on the parameter itself with `typing.Annotated`: `def run(upload_url: Annotated[str, "The presigned uploadUrl"])`. An `Annotated` description wins over `Args:`.
A YAML `manifest:` block inside the docstring is a **legacy** form. It is still read as a last-resort fallback for parameters nothing else describes, but it is deprecated — do not write new ones.
**File parameters:** Annotate a parameter as `bytes` to accept file\_id UUID strings. The platform downloads the file content from S3 before invoking the tool, so the function receives raw `bytes`. Example: `def run(data: bytes) -> dict:` — callers pass `{"data": ""}`.
| Parameter | Type | Required | Description |
| ------------ | ---------- | -------- | ----------- |
| env\_vars | `string[]` | no | |
| name | `string` | yes | |
| org | `string` | no | |
| source\_code | `string` | yes | |
| tags | `string[]` | no | |
### `create_prompt`
[Section titled “create\_prompt”](#create_prompt)
Create a prompt template. `text` may contain `{{variable}}` placeholders and is optional when `fields` alone defines a pure extraction schema.
`fields` is an optional list of field defs, each {name, field\_type, …}; valid field\_type: text, date, datetime, yes\_no, whole\_number, money, choice, object (choice needs a `choices` list). Any field can also set `is_list: true` to extract multiple values of that type instead of one. `object` fields nest a sub-schema under `fields` (a list of field defs, same shape, recursively) to extract a structured record; combine `field_type: "object"` with `is_list: true` to extract a list of records. When `fields` is set, the prompt runs as **structured extraction** instead of free-text generation — `run_prompt`/`test_prompt` return typed data, and `text` (if given) becomes the extraction instructions. `post_processing_instructions` (optional) runs a second refinement call over the extracted data.
`reasoning_effort` sets the LLM reasoning level (e.g. `"low"`, `"medium"`, `"high"`). `tags` attaches tag names to the new template.
| Parameter | Type | Required | Description |
| ------------------------------ | ---------- | -------- | ----------- |
| description | `string` | no | |
| fields | `object[]` | no | |
| model | `string` | no | |
| name | `string` | yes | |
| org | `string` | no | |
| post\_processing\_instructions | `string` | no | |
| reasoning\_effort | `string` | no | |
| tags | `string[]` | no | |
| text | `string` | no | |
### `create_skill`
[Section titled “create\_skill”](#create_skill)
Create a skill (a bundle of instructions + a tool subset + reference material). Created immediately active. `description` is a one-line summary kept in-context for agents that mount it. `tags` attaches tag names to the new skill.
Every entry in `tool_slugs` must resolve in this org — call `list_org_tools` (or `list_code_tools`) for the valid slugs.
| Parameter | Type | Required | Description |
| ------------------- | ---------- | -------- | ----------- |
| description | `string` | no | |
| instructions | `string` | yes | |
| name | `string` | yes | |
| org | `string` | no | |
| reference\_material | `string` | no | |
| tags | `string[]` | no | |
| tool\_slugs | `string[]` | no | |
### `get_code_tool`
[Section titled “get\_code\_tool”](#get_code_tool)
Get a code tool (by id or exact name), including its source code.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
| tool | `string` | yes | |
### `get_knowledge_base`
[Section titled “get\_knowledge\_base”](#get_knowledge_base)
Get a knowledge base (by id or slug), including its embedding model.
| Parameter | Type | Required | Description |
| --------------- | -------- | -------- | ----------- |
| knowledge\_base | `string` | yes | |
| org | `string` | no | |
### `get_prompt`
[Section titled “get\_prompt”](#get_prompt)
Get a prompt template (by id or slug), including its active text.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
| prompt | `string` | yes | |
### `get_skill`
[Section titled “get\_skill”](#get_skill)
Get a skill (by id or slug), including its active instructions and tools.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
| skill | `string` | yes | |
### `list_code_tools`
[Section titled “list\_code\_tools”](#list_code_tools)
List the org’s code tools (custom Python tools).
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
### `list_knowledge_bases`
[Section titled “list\_knowledge\_bases”](#list_knowledge_bases)
List the org’s knowledge bases, with source/chunk counts.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
### `list_prompts`
[Section titled “list\_prompts”](#list_prompts)
List the org’s prompt templates.
Paged: the response carries `total`, `has_more` and `next_offset` — pass `next_offset` back as `offset` to walk the rest.
| Parameter | Type | Required | Description |
| --------- | --------- | -------- | ---------------------------------------------------------- |
| limit | `integer` | no | Max rows to return (1–200). |
| offset | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org | `string` | no | |
### `list_skills`
[Section titled “list\_skills”](#list_skills)
List the org’s skills.
Paged: the response carries `total`, `has_more` and `next_offset` — pass `next_offset` back as `offset` to walk the rest.
| Parameter | Type | Required | Description |
| --------- | --------- | -------- | ---------------------------------------------------------- |
| limit | `integer` | no | Max rows to return (1–200). |
| offset | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org | `string` | no | |
### `search_knowledge_base`
[Section titled “search\_knowledge\_base”](#search_knowledge_base)
Search the org’s knowledge bases for relevant content chunks.
This is the same retrieval the in-chat `search_knowledge` agent tool uses: it matches both meaning and exact wording, so a single distinctive term works as well as a full question. Pass `knowledge_base` (an id or slug from `list_knowledge_bases`) to restrict the search to one base; omit it to search all of them.
| Parameter | Type | Required | Description |
| --------------- | --------- | -------- | ---------------- |
| knowledge\_base | `string` | no | |
| limit | `integer` | no | Max hits (1–25). |
| org | `string` | no | |
| query | `string` | yes | |
### `update_code_tool`
[Section titled “update\_code\_tool”](#update_code_tool)
Update a code tool (by id or exact name). Only the fields you pass are changed. `tags` replaces the tool’s current tag set (pass an empty list to clear all tags).
**A new `source_code` re-derives everything the model sees** the same way `create_code_tool` does: the entrypoint’s docstring (module docstring as a fallback) — first paragraph as the tool description, an `Args:` section for the parameters — and the annotations for the parameter types. A parameter can instead carry its own description via `Annotated[str, "..."]`, which wins over `Args:`. A YAML `manifest:` block in the docstring is a deprecated legacy form; when you touch a tool that has one, move its parameter prose into an `Args:` section.
**File parameters:** Annotate a parameter as `bytes` to accept file\_id UUID strings. The platform downloads the file content from S3 before invoking the tool, so the function receives raw `bytes`. Example: `def run(data: bytes) -> dict:` — callers pass `{"data": ""}`.
| Parameter | Type | Required | Description |
| ------------ | ---------- | -------- | ----------- |
| env\_vars | `string[]` | no | |
| is\_enabled | `boolean` | no | |
| name | `string` | no | |
| org | `string` | no | |
| source\_code | `string` | no | |
| tags | `string[]` | no | |
| tool | `string` | yes | |
### `update_prompt`
[Section titled “update\_prompt”](#update_prompt)
Update a prompt template (by id or slug). Passing `text`, `fields`, or `post_processing_instructions` creates a new revision (versioned together). Only the fields you pass are changed. `fields` (see `create_prompt` for the shape) turns the prompt into — or updates — structured extraction; valid field\_type: text, date, datetime, yes\_no, whole\_number, money, choice, object. Any field can set `is_list: true` to extract multiple values; `object` fields nest a sub-schema under `fields` for a structured record, and `object` + `is_list: true` extracts a list of records. `reasoning_effort` sets the LLM reasoning level. `tags` replaces the template’s current tag set (pass an empty list to clear all tags).
| Parameter | Type | Required | Description |
| ------------------------------ | ---------- | -------- | ----------- |
| description | `string` | no | |
| fields | `object[]` | no | |
| model | `string` | no | |
| name | `string` | no | |
| org | `string` | no | |
| post\_processing\_instructions | `string` | no | |
| prompt | `string` | yes | |
| reasoning\_effort | `string` | no | |
| tags | `string[]` | no | |
| text | `string` | no | |
### `update_skill`
[Section titled “update\_skill”](#update_skill)
Update a skill (by id or slug) and deploy the change so it takes effect (skills are live immediately, like on create). Only the fields you pass are changed. `tags` replaces the skill’s current tag set (pass an empty list to clear all tags).
Every entry in `tool_slugs` must resolve in this org — call `list_org_tools` (or `list_code_tools`) for the valid slugs.
| Parameter | Type | Required | Description |
| ------------------- | ---------- | -------- | ----------- |
| description | `string` | no | |
| instructions | `string` | no | |
| name | `string` | no | |
| org | `string` | no | |
| reference\_material | `string` | no | |
| skill | `string` | yes | |
| tags | `string[]` | no | |
| tool\_slugs | `string[]` | no | |
# Chats
> MCP server tools for chats.
### `get_chat`
[Section titled “get\_chat”](#get_chat)
Get a chat’s current state — status, full message history, recorded outcomes, cost, and any pending human-input request. Poll this until `status` is no longer “running”.
The `messages` history holds only user/assistant text turns. Human input requests and the answers they got are NOT messages — they come back in `hitl_requests` (every request for the chat, with its `response_text`), while `pending_hitl_request` holds only the one still waiting. When the request offered `ui` (buttons/choice/text/info — the same shape `request_human_input` accepts), it’s included on both so you can see exactly what a human would be asked to pick from before calling `respond_to_hitl`.
When the agent recorded an outcome, its summary IS the answer, and the last assistant message carries it (tagged `kind: "outcome_summary"`) even if the model’s own closing turn was empty — so `messages` alone is enough to read the result. `outcomes` remains the graded record.
`stop_reason` says why a chat is no longer moving: “cost\_limit” (raise the agent’s per-chat budget and start again), “awaiting\_input” (a human must answer), “timer” — the agent set its own timer and sleeps until `waiting_until`, so it is still working and nobody need act —, “queued” (waiting for a free slot), “awaiting\_children” (waiting on chats it spawned) and “environment\_offline” (its remote machine is not connected; the turn starts by itself when the machine comes back), all likewise self-resolving, “model\_unpriced” — the model the chat runs on has no price we can charge credits against, so the turn was refused before it was sent; the organisation’s balance is untouched and topping up changes nothing, so move the agent to another model or ask support to price this one. That stop and “credits\_exhausted” are both **continuable**: once the blocker is lifted, a person can carry the same chat on from where it stopped rather than starting over —, “goal\_failed” — the agent recorded a `failed` goal and closed the chat itself, so its `status` is “errored” by decision rather than by breakage and the summary says why — or “no\_outcome”, the agent replied and stopped without calling `goal_complete`, so `outcomes` is empty and nothing was declared. It is idle, not running: stop polling and treat the result as unverified.
Pass `include_trace=true` to also get a `trace` array of the underlying events (tool calls + results with name/args/result preview), oldest first — useful to see *why* the agent did something or which tool failed.
| Parameter | Type | Required | Description |
| -------------- | --------- | -------- | ----------- |
| chat\_id | `string` | yes | |
| include\_trace | `boolean` | no | |
| org | `string` | no | |
### `list_chats`
[Section titled “list\_chats”](#list_chats)
List recent chats for an agent (by id or slug), newest first.
Paged: the response carries `total`, `has_more` and `next_offset` — pass `next_offset` back as `offset` to walk the rest.
**`status` alone never means “finished”.** Chats have no terminal “completed” status — a chat that ran to a successful `goal_complete` still reports `status: "active"`, indistinguishable from one still working. To tell them apart, use the same two fields `get_chat` exposes:
* `outcome_status` — the chat’s rolled-up `outcomes` status (“success” / “failed” / “partial”), or `null` if it hasn’t recorded one yet. When a chat recorded more than one outcome, any `success` wins over an earlier failure/partial; otherwise the most recently recorded outcome’s status is reported.
* `stop_reason` — why the chat stopped moving: “cost\_limit”, “awaiting\_input”, “timer”, “queued”, “awaiting\_children”, “environment\_offline”, “goal\_failed”, “no\_outcome” (it replied and stopped without declaring a result), or `null` while it’s still running. See `get_chat`’s docstring for the full vocabulary.
A chat is actually finished when `outcome_status` is non-null (or `stop_reason` is one of the terminal reasons above) — not merely because `status` reads “active”.
| Parameter | Type | Required | Description |
| --------- | --------- | -------- | ---------------------------------------------------------- |
| agent | `string` | yes | |
| limit | `integer` | no | Max rows to return (1–200). |
| offset | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org | `string` | no | |
### `respond_to_hitl`
[Section titled “respond\_to\_hitl”](#respond_to_hitl)
Answer a pending human-input request (from a paused chat) and resume the agent. Use `action` (“approve”/“reject”, or one of the request’s `ui` `buttons`) for approvals and button choices, `selection` for one of the `ui` `choice` options, `selections` when the request asks more than one question, `text` for a free-text answer. At least one is required.
`get_chat`’s `pending_hitl_request.ui` always spells out what is on offer: a request that declared no `ui` of its own resolves to the default for its type, so an approval really does offer `Approve`/`Reject`. When the resolved `ui` lists `buttons`/`choice`, `action`/`selection` must be one of those options — a value that isn’t gets rejected with the valid options listed. A `ui` that is free-text only accepts anything.
**More than one question in one request.** Each `{"choice": [...]}` element in the resolved `ui` carries a `key` (and often a `label`). To answer them all, pass `selections` as `{key: chosen_value}` — e.g. `{"environment": "Staging", "rollback_window": "24 hours"}`. Every value is checked against its own group’s options. A bare `selection` is only accepted when the request offers a single choice group; otherwise it cannot say which question it answers and is rejected with the keys and their options listed. `selection` and `selections` are mutually exclusive — send one or the other, never both.
| Parameter | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| action | `string` | no | |
| org | `string` | no | |
| request\_id | `string` | yes | |
| selection | `string` | no | |
| selections | `object` | no | |
| text | `string` | no | |
### `send_message`
[Section titled “send\_message”](#send_message)
Send a user message to an existing chat and dispatch the agent turn. A message to a paused chat resumes it. Runs asynchronously — poll `get_chat` for the result.
To attach files, first use `create_upload_url` to get presigned upload URLs, PUT your files to those URLs, then pass the returned `upload_id`s in `attachment_upload_ids`. Each `upload_id` is single-use: attaching is all-or-nothing, so if any id has already been attached (or has expired) the call is refused naming that id, and no message is sent.
| Parameter | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| attachment\_upload\_ids | `string[]` | no | |
| chat\_id | `string` | yes | |
| message | `string` | yes | |
| org | `string` | no | |
### `start_chat`
[Section titled “start\_chat”](#start_chat)
Start a new chat with an agent (by id or slug) and send the first message. Returns the `chat_id`; the turn runs asynchronously — poll `get_chat` for the result.
By default the chat runs the agent’s **published** (active) revision. To test an unpublished instruction without deploying it, pass `use_draft=true` (pins the agent’s current draft revision) OR `revision_version=N` (pins a specific past version). The pin holds for the chat’s whole life. The two options are mutually exclusive.
**Where the chat runs is `environment`.** Pass an environment’s slug, id, or unambiguous name — `list_environments` shows what this org has. A `remote_daemon` environment is a machine the user owns: their files, their installed tools, in place. Nothing else reaches that machine, so a request to run something *on the user’s computer* needs this argument; without it the chat runs where the org’s default puts it, and a command that only exists on their machine fails there for reasons that look nothing like the real cause.
`sandbox_enabled=true` is the narrower, older switch: it turns on **our built-in Linux sandbox** — an isolated filesystem + network on our infrastructure, persisting across the conversation — for substantial coding, data, or file-heavy work. It only ever adds capability on top of the agent’s own sandbox setting, and it is not a way to reach any particular machine.
`model_mode_override` (‘trivial’ | ‘normal’ | ‘high\_effort’ | ‘x\_high’, shown as Trivial / Standard / High / X-High) changes the agent’s own `model_mode` for this chat only — the one model lever an org whose plan selects effort rather than models has, in either direction: `trivial` drops routine work onto a cheaper model, `high_effort` and `x_high` buy dearer ones. It is sticky: it applies to this turn and every later `send_message` on the chat. Only meaningful for such an org; an org with direct model choice is refused it (the value would be inert) and picks a model on the agent instead.
To attach files, first use `create_upload_url` to get presigned upload URLs, PUT your files to those URLs, then pass the returned `upload_id`s in `attachment_upload_ids`. Each `upload_id` is single-use: attaching is all-or-nothing, so if any id has already been attached (or has expired) the call is refused naming that id, and no chat is created.
| Parameter | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| agent | `string` | yes | |
| attachment\_upload\_ids | `string[]` | no | |
| environment | `string` | no | |
| message | `string` | yes | |
| model\_mode\_override | `string` | no | |
| org | `string` | no | |
| revision\_version | `integer` | no | |
| sandbox\_enabled | `boolean` | no | |
| title | `string` | no | |
| use\_draft | `boolean` | no | |
# Documentation
> MCP server tools for documentation.
### `read_documentation`
[Section titled “read\_documentation”](#read_documentation)
Read AgentDepot documentation (what the platform is and how to use it).
Call with no `topic` to get the index, then with a topic slug (e.g. “overview”, “build-a-flow”, “testing”, “processes”, “intake”, “concepts”, “agent-environments”, “chat-lifecycle”, “files”, “prompt-fields”, “agent-mentions”) for that section. Use this to orient before setting up agents and flows.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| topic | `string` | no | |
# Files
> MCP server tools for files.
### `create_upload_url`
[Section titled “create\_upload\_url”](#create_upload_url)
Create a presigned PUT URL for uploading a file.
Step 1 of attaching a local file to a chat. Returns a temporary upload URL that you can PUT file bytes to directly.
Usage: 1. Call this tool to get an upload URL 2. PUT the file bytes to the returned `upload_url` with headers: - `Content-Type: ` 3. Pass the returned `upload_id` to `start_chat` or `send_message` in the `attachment_upload_ids` parameter
The upload URL expires in 15 minutes. Max file size is 10 MB.
`filename` is reduced to a safe single path segment before it is used — directory components are dropped and the extension is preserved — so the stored name may differ from what you passed.
| Parameter | Type | Required | Description |
| ------------- | --------- | -------- | ----------- |
| content\_type | `string` | no | |
| filename | `string` | yes | |
| org | `string` | no | |
| size | `integer` | no | |
### `list_files`
[Section titled “list\_files”](#list_files)
List all files in a chat — both user uploads and agent-produced artifacts — as a unified file list.
`chat_id` is required. The returned objects each carry `id`, `kind` (`"upload"` or `"artifact"`), `type`, `name`, `mime_type`, `size_bytes`, `preview` (the opening text — 500 chars for an artifact, 250 for an upload, `null` when the file has no extractable text), `created_at` and `is_dry_run` (a dry-run chat’s draft — listed here so it can be reviewed).
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| chat\_id | `string` | yes | |
| org | `string` | no | |
### `read_file`
[Section titled “read\_file”](#read_file)
Read a file or artifact by id, returning its text content.
`file_id` may refer to a chat upload (File) or an agent-produced artifact (Artifact) — both are resolved transparently.
`mode` controls extraction behaviour:
* `"auto"` (default) — try text extraction; if the MIME type is not supported (e.g. `image/jpeg`), return metadata + a `note` instead of raising an error.
* `"text"` — force text extraction; raises an error if the file type is not extractable as text.
* `"visual"` — always return metadata + note without attempting extraction (useful when you know you have an image and just want the metadata).
The response always contains `id`, `name`, `mime_type`, `size_bytes` and `is_dry_run` (true for a file a dry-run chat wrote — a draft for review, not a delivered result). Successful extraction adds `content`; unsupported types add `note`.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| file\_id | `string` | yes | |
| mode | `string` | no | |
| org | `string` | no | |
### `search_files`
[Section titled “search\_files”](#search_files)
Search files (uploads and artifacts) across the org.
All filters are optional; combine them to narrow the result set:
* `chat_id` — restrict to one chat’s files.
* `agent` — restrict to one agent (id or slug): the artifacts it produced and the uploads in its chats. An upload has no agent column of its own, so it is scoped through its owning chat.
* `title_contains` — case-insensitive substring match on the filename or artifact title.
* `type` — `"file"` to search uploads only; an artifact type value (`"text"`, `"code"`, `"markdown"`, `"json"`) to search artifacts only.
* `limit` — max results (default 20, max 100).
* `include_dry_run` — also return files written by dry-run chats (drafts for review, each flagged `is_dry_run`). Off by default so an org-wide search never hands a rehearsal’s output to something that would act on it; always on when `chat_id` names one chat.
Results are returned newest-first.
| Parameter | Type | Required | Description |
| ----------------- | --------- | -------- | -------------------- |
| agent | `string` | no | |
| chat\_id | `string` | no | |
| include\_dry\_run | `boolean` | no | |
| limit | `integer` | no | Max results (1–100). |
| org | `string` | no | |
| title\_contains | `string` | no | |
| type | `string` | no | |
# Integrations
> MCP server tools for integrations.
### `disable_integration`
[Section titled “disable\_integration”](#disable_integration)
Disable an integration (by id or slug) so agents no longer see its tools — sets status to “inactive”. Requires ADMIN or OWNER role.
| Parameter | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| integration | `string` | yes | |
| org | `string` | no | |
### `enable_integration`
[Section titled “enable\_integration”](#enable_integration)
Enable an integration (by id or slug) so its tools are exposed to agents — sets status to “active”. Requires ADMIN or OWNER role.
| Parameter | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| integration | `string` | yes | |
| org | `string` | no | |
### `list_integrations`
[Section titled “list\_integrations”](#list_integrations)
List the org’s integrations / connections.
Each has `status` and an `enabled` bool (the web UI toggle). A disabled integration exposes NO tools to agents — so an agent whose `allowed_tools` references a disabled integration will run with none of those tools. Use `enable_integration` to turn one on. Connecting a brand-new integration still happens in the web UI (often needs interactive OAuth).
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
# Organizations
> MCP server tools for organizations.
### `list_orgs`
[Section titled “list\_orgs”](#list_orgs)
List the organizations you can act in.
Pass an org’s `slug` (or `id`) as the `org` argument to other tools. If you belong to exactly one org, `org` can be omitted everywhere.
*No parameters.*
# Skills
> MCP server tools for skills.
### `get_system_skill`
[Section titled “get\_system\_skill”](#get_system_skill)
Get a platform/system skill by slug, including full instructions.
Returns:
* `slug`, `name`, `description` — identity fields
* `version` — active revision number
* `tool_slugs` — MCP tool names the skill uses
* `agent_only_tool_slugs` — tools the skill uses that this surface does not expose (see below)
* `instructions` — the full step-by-step procedure to follow
* `reference_material` — optional supplementary reference text
Reading `instructions` tells you exactly how to carry out the skill. Every tool in `tool_slugs` is exposed by the `agents`, `building_blocks`, `chats`, `schedules` and `integrations` MCP modules — call them as directed.
**`instructions` is written for a running agent**, so its prose names tools in the agent-runtime vocabulary. Two things follow. Some names are spelled differently here (e.g. the prose says `create_custom_tool`; the MCP tool is `create_code_tool`) — `tool_slugs` already gives you the MCP spelling, in the same order. And the steps using a tool listed in `agent_only_tool_slugs` cannot be carried out from an MCP client at all: integration discovery/connect, self-modification and memory are agent-run concerns. Do the rest of the procedure and say plainly which steps you skipped.
Raises a ToolError if the slug does not match any active system skill. Use `list_system_skills` to see available slugs.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| slug | `string` | yes | |
### `list_system_skills`
[Section titled “list\_system\_skills”](#list_system_skills)
List all active platform/system skills available on this deployment.
System skills are global — they are not scoped to any org and are the same for every authenticated user. Each entry contains:
* `slug` — stable identifier, use with `get_system_skill`
* `description` — one-line summary
* `version` — current active revision number
* `tool_slugs` — MCP tool names the skill relies on, callable here
* `agent_only_tool_slugs` — steps of this skill that only a running agent can perform; there is no MCP tool for these
Call `get_system_skill(slug)` to retrieve the full `instructions` for a specific skill before executing it.
*No parameters.*
# Teams
> MCP server tools for teams.
### `create_team`
[Section titled “create\_team”](#create_team)
Create a new agent team.
The three `shared_*` fields form the team’s shared context, merged with each member agent’s own configuration at prompt-assembly time: `shared_instruction` is prepended to member instructions; `shared_allowed_tools` is a list of tool scope strings or slugs granted to all members; `shared_mounted_skills` is a list of skill slugs mounted on all members. Add agents to the team afterwards with `set_agent_team`. The slug is derived from `name` (deduped within the org).
Both lists are checked against the org: every `shared_allowed_tools` entry must resolve in the tool catalog (`list_org_tools`) and every `shared_mounted_skills` entry must be an existing skill slug (`list_skills`).
| Parameter | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | yes | |
| org | `string` | no | |
| shared\_allowed\_tools | `string[]` | no | |
| shared\_instruction | `string` | no | |
| shared\_mounted\_skills | `string[]` | no | |
### `delete_team`
[Section titled “delete\_team”](#delete_team)
Delete a team (by id or slug). Member agents are NOT deleted — they become ungrouped and revert to their own configuration.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
| team | `string` | yes | |
### `get_team`
[Section titled “get\_team”](#get_team)
Get full detail for one team (by id or slug): its shared context (shared instruction, allowed tools, mounted skills) and the agents that belong to it.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| org | `string` | no | |
| team | `string` | yes | |
### `list_teams`
[Section titled “list\_teams”](#list_teams)
List the agent teams in an organization, ordered for display.
Each entry includes the team’s slug, name, description, and current member count. Call `get_team` for a team’s shared context and member list.
Paged: the response carries `total`, `has_more` and `next_offset` — pass `next_offset` back as `offset` to walk the rest.
| Parameter | Type | Required | Description |
| --------- | --------- | -------- | ---------------------------------------------------------- |
| limit | `integer` | no | Max rows to return (1–200). |
| offset | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org | `string` | no | |
### `set_agent_team`
[Section titled “set\_agent\_team”](#set_agent_team)
Set (or clear) an agent’s team membership.
Pass `team` (id or slug) to move the agent onto that team — it then inherits the team’s shared context. Omit `team` (or pass an empty string) to remove the agent from any team, reverting it to its own configuration.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| agent | `string` | yes | |
| org | `string` | no | |
| team | `string` | no | |
### `update_team`
[Section titled “update\_team”](#update_team)
Update a team (by id or slug). Partial: only the fields you pass are changed; omit the rest to leave them untouched.
Renaming (`name`) re-derives the slug. `shared_allowed_tools` and `shared_mounted_skills` REPLACE the current lists (pass an empty list to clear). Pass an empty string for `shared_instruction` to clear it. Changes apply immediately to every current member’s assembled prompt. Both lists are checked against the org before they are stored — see `create_team`.
| Parameter | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | no | |
| org | `string` | no | |
| shared\_allowed\_tools | `string[]` | no | |
| shared\_instruction | `string` | no | |
| shared\_mounted\_skills | `string[]` | no | |
| team | `string` | yes | |
# Testing
> MCP server tools for testing.
### `test_code_tool`
[Section titled “test\_code\_tool”](#test_code_tool)
Run a code tool (by id or name) in isolation with the given `args` and return its result, logs, and any error — without wiring it into an agent. Runs the tool’s current saved source in a Docker sandbox. `args` are passed straight to the tool as keyword arguments.
**File arguments:** For parameters typed as `bytes` in the tool source, pass a file\_id UUID string as the value. The platform downloads the file content from S3 and delivers raw `bytes` to the tool function. Example: `{"data": ""}` for a tool with `def run(data: bytes)`. You may also pass an `upload_id` from `create_upload_url` (after PUTting the bytes) to test against a freshly-uploaded local file without first sending it through a chat.
`args` are checked against the tool’s `parameters_schema` before anything runs — a missing required argument or an unknown one fails immediately instead of starting a container.
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------- |
| args | `object` | no | |
| org | `string` | no | |
| tool | `string` | yes | |
### `test_prompt`
[Section titled “test\_prompt”](#test_prompt)
Test a prompt template (by id or slug). Behavior depends on whether the tested revision has an extraction schema (`fields`, see `create_prompt`):
* **Plain prompt** (no `fields`): renders `text` with `variables` and runs it against the configured LLM, returning the rendered text and the model’s free-text output.
* **Schema-bearing prompt** (`fields` set): runs structured extraction and returns typed data instead. Provide the material to extract from via `input_text` (raw text) or `input_url` (fetched and used as the input) — one of the two is required. `variables` still interpolates any `{{name}}` placeholders in the prompt’s instructions.
Every variable the tested revision declares must be supplied in `variables` — a missing one fails before the model is called, rather than sending the raw `{{placeholder}}` to it.
Pass `revision_version` to test a specific (e.g. unpublished) revision; omit it to use the latest.
**Model selection:** pass `model` to test with a specific model — otherwise the prompt’s own stored model runs, or the platform default when the prompt names none. With neither set the call is refused rather than run on a guess: set a model on the prompt. Rejected with 422 for an org whose plan sells effort *levels* rather than model choice. Such an org instead passes `model_mode` (‘trivial’ | ‘normal’ | ‘high\_effort’ | ‘x\_high’, shown as Trivial / Standard / High / X-High) — the platform decides what that level runs (model + reasoning effort + thinking); omit it to run on the default level (Standard). `model` and `model_mode` are the two halves of the same choice: whichever one this org’s plan doesn’t sell is refused with 422 rather than silently ignored.
`file_ids` is an optional list of ids to attach to the prompt as multimodal content — text documents become text blocks, images and scanned PDFs become image blocks. Each id may be a durable file\_id (a chat upload or artifact) OR an `upload_id` from `create_upload_url` (after you’ve PUT the bytes), so you can test against a freshly-uploaded local file without first sending it through a chat. Uploads are resolved only within the caller’s organization.
`file_mode` optionally overrides how attached files render into the LLM message: `auto` (default — text if extractable else image), `image`, `text`, or `both`. Omit to use the prompt template’s configured mode.
| Parameter | Type | Required | Description |
| ----------------- | ---------- | -------- | ----------- |
| file\_ids | `string[]` | no | |
| file\_mode | `string` | no | |
| input\_text | `string` | no | |
| input\_url | `string` | no | |
| model | `string` | no | |
| model\_mode | `string` | no | |
| org | `string` | no | |
| prompt | `string` | yes | |
| revision\_version | `integer` | no | |
| variables | `object` | no | |
# REST API Reference
> Public HTTP endpoints of the AgentDepot API.
The **AgentDepot API** (v0.1.0) exposes the following resource groups.
## Base URL
[Section titled “Base URL”](#base-url)
```plaintext
https://api.agentdepot.org
```
Every path in this reference already includes its `/api` prefix — append it to the base URL as-is. For example, listing the skills of an organization:
```bash
curl https://api.agentdepot.org/api/orgs/{org_id}/skills \
-H "Authorization: Bearer agd_..."
```
## Authentication
[Section titled “Authentication”](#authentication)
All endpoints require an `Authorization: Bearer ` header carrying an `agd_*` API token (see [API Tokens](/docs/reference/rest/tokens)).
## Resource groups
[Section titled “Resource groups”](#resource-groups)
* [Admin](/docs/reference/rest/admin)
* [Admin Ai](/docs/reference/rest/admin-ai)
* [Admin Autopin](/docs/reference/rest/admin-autopin)
* [Admin Billing](/docs/reference/rest/admin-billing)
* [Admin Costs](/docs/reference/rest/admin-costs)
* [Admin Credits](/docs/reference/rest/admin-credits)
* [Admin Experiments](/docs/reference/rest/admin-experiments)
* [Admin Users](/docs/reference/rest/admin-users)
* [Agent Chats](/docs/reference/rest/agent-chats)
* [Agent Memories](/docs/reference/rest/agent-memories)
* [Agent Rollouts](/docs/reference/rest/agent-rollouts)
* [Agent Teams](/docs/reference/rest/agent-teams)
* [Agent Toolkit](/docs/reference/rest/agent-toolkit)
* [Agents](/docs/reference/rest/agents)
* [Alerts](/docs/reference/rest/alerts)
* [Artifacts](/docs/reference/rest/artifacts)
* [Authentication](/docs/reference/rest/auth)
* [Billing](/docs/reference/rest/billing)
* [Chat Files](/docs/reference/rest/chat-files)
* [Chats](/docs/reference/rest/chats)
* [Connections](/docs/reference/rest/connections)
* [Conversation Resources](/docs/reference/rest/conversation-resources)
* [Credits](/docs/reference/rest/credits)
* [Custom Tools](/docs/reference/rest/custom-tools)
* [Decisions](/docs/reference/rest/decisions)
* [Effort](/docs/reference/rest/effort)
* [Envd](/docs/reference/rest/envd)
* [Environments](/docs/reference/rest/environments)
* [Files](/docs/reference/rest/files)
* [Human-in-the-Loop](/docs/reference/rest/hitl)
* [Inbox](/docs/reference/rest/inbox)
* [Inboxes](/docs/reference/rest/inboxes)
* [Intake](/docs/reference/rest/intake)
* [Composio Integrations](/docs/reference/rest/integrations-composio)
* [Invitations](/docs/reference/rest/invitations)
* [Knowledge](/docs/reference/rest/knowledge)
* [Library](/docs/reference/rest/library)
* [Members](/docs/reference/rest/members)
* [Models](/docs/reference/rest/models)
* [Org Inference Routes](/docs/reference/rest/org-inference-routes)
* [Org Integrations](/docs/reference/rest/org-integrations)
* [Org Models](/docs/reference/rest/org-models)
* [Org Settings](/docs/reference/rest/org-settings)
* [Org Variables](/docs/reference/rest/org-variables)
* [Organizations](/docs/reference/rest/organizations)
* [Overview](/docs/reference/rest/overview)
* [Processes](/docs/reference/rest/processes)
* [Projects](/docs/reference/rest/projects)
* [Prompt Templates](/docs/reference/rest/prompt-templates)
* [Resources](/docs/reference/rest/resources)
* [Schedules](/docs/reference/rest/schedules)
* [Search](/docs/reference/rest/search)
* [Skills](/docs/reference/rest/skills)
* [Spend](/docs/reference/rest/spend)
* [Tags](/docs/reference/rest/tags)
* [API Tokens](/docs/reference/rest/tokens)
* [Tool Sources](/docs/reference/rest/tool-sources)
* [Usage](/docs/reference/rest/usage)
# Admin
> REST API reference for admin.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/admin/admins`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/admin/admins`
[Section titled “GET /api/admin/admins”](#get-apiadminadmins)
List Admins
List all platform admins, joined to user + adder info.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `PlatformAdminResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/admins`
[Section titled “POST /api/admin/admins”](#post-apiadminadmins)
Add Admin
Grant platform-admin access to an existing, already-registered user.
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| email | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 201 | Successful Response | `PlatformAdminResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/admin/admins/{user_id}`
[Section titled “DELETE /api/admin/admins/{user\_id}”](#delete-apiadminadminsuser_id)
Remove Admin
Revoke platform-admin access. Guards: no self-removal, no removing the last admin.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| user\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/allowed-emails`
[Section titled “GET /api/admin/allowed-emails”](#get-apiadminallowed-emails)
List Allowed Emails
List all registration-allowlist entries.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `AllowedEmailResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/allowed-emails`
[Section titled “POST /api/admin/allowed-emails”](#post-apiadminallowed-emails)
Add Allowed Email
Add an entry to the registration allowlist.
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| email | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 201 | Successful Response | `AllowedEmailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/admin/allowed-emails/{allowed_email_id}`
[Section titled “DELETE /api/admin/allowed-emails/{allowed\_email\_id}”](#delete-apiadminallowed-emailsallowed_email_id)
Remove Allowed Email
Remove a registration-allowlist entry.
**Parameters**
| Name | In | Type | Required | Description |
| ------------------ | ---- | --------------- | -------- | ----------- |
| allowed\_email\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs`
[Section titled “GET /api/admin/orgs”](#get-apiadminorgs)
List Orgs
List/search all organizations, cross-tenant, with per-org member counts, plan, suspension state and remaining credits.
Subscription and balance are LEFT joins on purpose: an org with neither is exactly the state worth spotting from the list (no subscription = every entitlement denied), so it stays a row with null plan rather than vanishing. `credits_remaining` is the gate’s own expression — see `OrgListItem`. It folds the overdraft grace in, so `credits_balance` (owned) and `credits_overdraft` (lent) ride alongside it: a single figure that mixes the two reads as more credits than the org has.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ----- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| plan\_code | query | `string` | no | Only orgs on this plan. Pass `__none__` for orgs with no subscription row at all — every entitlement is denied for those, and they need a backfill. |
| q | query | `string` | no | Search org name/slug (substring, case-insensitive) |
| suspended | query | `boolean` | no | true = only suspended orgs, false = only active. Omit for both. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs/{org_id}`
[Section titled “GET /api/admin/orgs/{org\_id}”](#get-apiadminorgsorg_id)
Get Org
Org detail plus its member roster.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs/{org_id}/audit`
[Section titled “GET /api/admin/orgs/{org\_id}/audit”](#get-apiadminorgsorg_idaudit)
Get Org Audit
Change history for one org — who changed what, when.
Merges the two trails that can say something about an org, newest first:
* `admin_audit_log` — actions taken through this admin surface, plus webhook-driven plan changes recorded with a **system** actor;
* `audit_log` — Layer-2 semantic business events service code records against the org (`token.revoked`, …).
Deliberately *not* included: Layer-1 raw row capture (`audit.record_version`). It has no org column to scope by and one page-load of agent traffic would bury every deliberate change in row diffs.
The merge happens in Python rather than SQL: the two tables share no column names or types, and both are small enough here that over-fetching `limit + offset` from each is cheaper than the UNION plumbing.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgAuditResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/orgs/{org_id}/members`
[Section titled “POST /api/admin/orgs/{org\_id}/members”](#post-apiadminorgsorg_idmembers)
Add Org Member
Add an EXISTING, already-registered user to an org. Never creates users or sends invites.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| email | `string` | yes | |
| role | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `OrgMemberInfo` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/admin/orgs/{org_id}/members/{user_id}`
[Section titled “PATCH /api/admin/orgs/{org\_id}/members/{user\_id}”](#patch-apiadminorgsorg_idmembersuser_id)
Update Org Member Role
Change a member’s role. Refuses if it would leave the org with 0 owners.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| user\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| role | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgMemberInfo` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/admin/orgs/{org_id}/members/{user_id}`
[Section titled “DELETE /api/admin/orgs/{org\_id}/members/{user\_id}”](#delete-apiadminorgsorg_idmembersuser_id)
Remove Org Member
Remove a member from an org. Refuses to remove the last OWNER.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| user\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/orgs/{org_id}/suspend`
[Section titled “POST /api/admin/orgs/{org\_id}/suspend”](#post-apiadminorgsorg_idsuspend)
Suspend Org
Suspend an organization — blocks regular member access via `get_current_org`.
Idempotent: calling this on an already-suspended org is a no-op (keeps the original `suspended_at` timestamp, 200, no duplicate audit entry) rather than 409 — this is a “make it so” action, safe to double-click from the UI.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `OrgSuspensionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/orgs/{org_id}/unsuspend`
[Section titled “POST /api/admin/orgs/{org\_id}/unsuspend”](#post-apiadminorgsorg_idunsuspend)
Unsuspend Org
Lift a suspension. Idempotent no-op if the org isn’t currently suspended.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `OrgSuspensionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs/{org_id}/usage`
[Section titled “GET /api/admin/orgs/{org\_id}/usage”](#get-apiadminorgsorg_idusage)
Get Org Usage
Usage summary for one org — same shape/service as the self-serve `/api/orgs/{org_id}/usage/summary` endpoint, just without membership gating.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ----------------------------------------------- |
| org\_id | path | `string (uuid)` | yes | |
| window | query | `string` | no | Counter/cost window: 24h, 7d, 30d (default 30d) |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `UsageSummaryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/subscriptions/backfill`
[Section titled “POST /api/admin/subscriptions/backfill”](#post-apiadminsubscriptionsbackfill)
Backfill Subscriptions
Give every org that lacks an `org_subscriptions` row one. Re-runnable.
The repair path for a window that already happened: orgs created between the `plans`/`org_subscriptions` migration (which backfilled everyone alive at the time) and the org-creation hook have no subscription, so they resolve to `UNSUBSCRIBED` — every feature denied, every count cap 0. Creation now subscribes in the same transaction as the org insert, but that fixes nothing retroactively.
Each org subscribed here also receives the plan’s **credit grant and overdraft floor**, in the same transaction — a subscription without them is an org that cannot do any work (plan §4.4). The default plan (enterprise) allocates nothing, so the usual repair run writes no ledger rows.
Idempotent, and cheap to run blind: the underlying `INSERT ... SELECT` only sees orgs with no subscription row, so a second call inserts nothing, returns `created: 0` and writes no audit entry. The grant carries its own once-ever-per-org-per-plan key on top, so it cannot double-allocate even if the subscription row is later recreated. It never modifies an existing subscription — a plan *change* is a separate, deliberate admin action, not something a repair sweep should do by accident. That makes this safe to run after any deploy, and safe to double-click.
It is **not** a way to top an org up: an org that already has a subscription gets nothing here, and one whose grant went missing is repaired with an `adjustment`.
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| org\_id | `string (uuid)` | no | Repair only this organization. Omit to repair every organization that has no subscription row. |
| plan\_code | `string` | no | Plan to place missing subscriptions on. Defaults to CREDITS\_DEFAULT\_NEW\_ORG\_PLAN — the same plan org creation uses — so a repaired org is indistinguishable from a freshly created one. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `SubscriptionBackfillResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/users/{user_id}/deactivate`
[Section titled “POST /api/admin/users/{user\_id}/deactivate”](#post-apiadminusersuser_iddeactivate)
Deactivate User
Disable a user’s account. Guard: no self-deactivate.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| user\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `UserStatusResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/users/{user_id}/reactivate`
[Section titled “POST /api/admin/users/{user\_id}/reactivate”](#post-apiadminusersuser_idreactivate)
Reactivate User
Re-enable a previously deactivated user’s account.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| user\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `UserStatusResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/waitlist`
[Section titled “GET /api/admin/waitlist”](#get-apiadminwaitlist)
List Waitlist
List waitlist signups, most recently asked first.
**Parameters**
| Name | In | Type | Required | Description |
| ------ | ----- | ---------------- | -------- | --------------------------------------- |
| limit | query | `integer` | no | |
| status | query | `WaitlistStatus` | no | Only rows in this status. Omit for all. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `WaitlistListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/waitlist/{waitlist_id}/dismiss`
[Section titled “POST /api/admin/waitlist/{waitlist\_id}/dismiss”](#post-apiadminwaitlistwaitlist_iddismiss)
Dismiss From Waitlist
Decline a waitlist signup.
Keeps the row. Deleting it would let the same address walk straight back into the pending queue on its next submit, and the decision would have to be taken again every time.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| waitlist\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `WaitlistEntryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/waitlist/{waitlist_id}/invite`
[Section titled “POST /api/admin/waitlist/{waitlist\_id}/invite”](#post-apiadminwaitlistwaitlist_idinvite)
Invite From Waitlist
Admit somebody from the waitlist and tell them so.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| waitlist\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `WaitlistEntryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/waitlist/{waitlist_id}/resend`
[Section titled “POST /api/admin/waitlist/{waitlist\_id}/resend”](#post-apiadminwaitlistwaitlist_idresend)
Resend Waitlist Invite
Mail an already-admitted person again.
For the row whose admission stuck but whose mail did not. Refuses on a row that was never admitted — sending “your invitation is ready” to somebody who is still refused at the door is worse than sending nothing.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| waitlist\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `WaitlistEntryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Admin Ai
> REST API reference for admin ai.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/admin/ai-settings`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/admin/ai-settings`
[Section titled “GET /api/admin/ai-settings”](#get-apiadminai-settings)
Get Platform Ai Settings
Current platform AI scalars. Null fields defer to the env defaults.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `PlatformAISettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/admin/ai-settings`
[Section titled “PATCH /api/admin/ai-settings”](#patch-apiadminai-settings)
Update Platform Ai Settings
Update platform AI scalars.
Values land in `platform_settings` and are re-read per run, so a change here reaches running workers within their catalog/settings TTL — no deploy.
The default model is checked against the catalog before it is stored: it is what every org without a default of its own runs on, so a typo’d or retired id here would silently break all of them at the next turn instead of here, now, with a list of the ids that would have worked.
Feature models are checked the same way and against a harder deadline: an uncatalogued id there is not a broken call, it is a *working* one that bills nothing while the platform pays the provider, and nothing downstream would report it.
**Request body** (required)
| Field | Type | Required | Description |
| -------------------------- | --------------------------- | -------- | ----------- |
| compaction\_enabled | `boolean` | no | |
| compaction\_threshold | `number` | no | |
| default\_model | `PlatformDefaultModelInput` | no | |
| default\_reasoning\_effort | `string` | no | |
| feature\_models | `object` | no | |
| max\_reasoning\_effort | `string` | no | |
| mode\_high\_effort | `ModePresetInput` | no | |
| mode\_normal | `ModePresetInput` | no | |
| mode\_trivial | `ModePresetInput` | no | |
| mode\_x\_high | `ModePresetInput` | no | |
| thinking\_enabled | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `PlatformAISettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/ai/route-keys`
[Section titled “GET /api/admin/ai/route-keys”](#get-apiadminairoute-keys)
Get Platform Route Keys
Which key each inference route would spend, masked, with its provenance.
Note `is_set` and `source` can disagree: a stored route key that no longer decrypts reads as set *and* falls through to `legacy`/`env`, which is exactly the state a rotated ENCRYPTION\_KEY produces.
Its own path rather than a field on `GET /ai/routes` — the route list is not a secret-adjacent read, and keeping them apart means a client that only renders routes never asks for key state at all.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `PlatformRouteKeysResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/admin/ai/route-keys/{route_id}`
[Section titled “PUT /api/admin/ai/route-keys/{route\_id}”](#put-apiadminairoute-keysroute_id)
Set Platform Route Key
Store or clear one route’s platform key.
Stored encrypted, in a row created on first write — which is what makes this surface open-ended: a route created in the admin panel has a working key slot immediately, with no code change, no def and no migration. Clearing falls back down the ladder (legacy provider key, then the env var) rather than to nothing.
Refused for a route that spends another route’s key: the whole point of `credential_route_id` is that the secret is stored once, and accepting a write here would create the second copy it exists to prevent. The error names the route to edit instead.
The cache is dropped after the commit, so a rotation reaches the API and every agent-runner immediately instead of within the cache TTL.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| route\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------- | -------- | -------- | ----------- |
| api\_key | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `PlatformRouteKeysResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/ai/routes`
[Section titled “GET /api/admin/ai/routes”](#get-apiadminairoutes)
List Inference Routes
Every route, enabled or not, in display order.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `InferenceRouteResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/ai/routes`
[Section titled “POST /api/admin/ai/routes”](#post-apiadminairoutes)
Create Inference Route
Add a route. `slug` must be unique — it’s what catalog rows will store.
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | ----------------------------------------- | -------- | ----------- |
| base\_url | `string` | no | |
| credential\_route\_id | `string (uuid)` | no | |
| dialect | `"anthropic"` \| `"openai"` \| `"voyage"` | yes | |
| enabled | `boolean` | no | |
| gateway\_provider\_slug | `string` | no | |
| label | `string` | yes | |
| slug | `string` | yes | |
| sort\_order | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 201 | Successful Response | `InferenceRouteResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/ai/routes/{route_id}`
[Section titled “GET /api/admin/ai/routes/{route\_id}”](#get-apiadminairoutesroute_id)
Get Inference Route
One route by id.
Exists so a detail view gets a real 404 for a deleted or mistyped id, rather than deriving “not found” from a successful list response — which reports the same condition as an empty filter, and cannot distinguish “this route is gone” from “the list call failed and returned nothing”.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| route\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `InferenceRouteResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/admin/ai/routes/{route_id}`
[Section titled “PATCH /api/admin/ai/routes/{route\_id}”](#patch-apiadminairoutesroute_id)
Update Inference Route
Update a route.
Switching `enabled` off is **refused with 409** while anything is still pinned to it, naming what — there is no lazy retirement for routes, so a disabled route is a hard fail at dispatch rather than a redirect, and the blast radius of getting it wrong is a mode preset taking down every trivial-effort surface at once. `POST /ai/routes/{id}/repoint` is what makes the refusal satisfiable; `GET /ai/routes/{id}/pins` is the same answer without attempting the write.
**Which half of the `enabled` asymmetry this extends.** Route `enabled` gates *dispatch through* a route and deliberately not *credential resolution*: `resolve_credential_route` walks through disabled rows so that switching `anthropic-direct` off to push everyone onto `anthropic-cf` does not also strip the gateway route of the key it spends. This guard is on the **dispatch** side — it counts what would stop dispatching, and asks nothing about who spends whose key. The credential side already has its own, separate refusal: `_ensure_usable_credential_route` rejects a *disabled* route as somebody’s `credential_route_id`, and the delete path refuses while dependants exist.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| route\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | ----------------------------------------- | -------- | ----------- |
| base\_url | `string` | no | |
| credential\_route\_id | `string (uuid)` | no | |
| dialect | `"anthropic"` \| `"openai"` \| `"voyage"` | no | |
| enabled | `boolean` | no | |
| gateway\_provider\_slug | `string` | no | |
| label | `string` | no | |
| sort\_order | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `InferenceRouteResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/admin/ai/routes/{route_id}`
[Section titled “DELETE /api/admin/ai/routes/{route\_id}”](#delete-apiadminairoutesroute_id)
Delete Inference Route
Remove a route.
Refused with 409 while another route spends this one’s key. Not a cascade and not a null-out: promoting `anthropic-cf` to “spends its own key” because `anthropic-direct` was deleted would send requests authenticated with a key nobody ever configured, and the first anyone hears of it is a provider 401 on live traffic. Repoint the dependants first.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| route\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/ai/routes/{route_id}/pins`
[Section titled “GET /api/admin/ai/routes/{route\_id}/pins”](#get-apiadminairoutesroute_idpins)
Get Inference Route Pins
What is pinned to this route, without attempting a write.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| route\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `RoutePinsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/ai/routes/{route_id}/repoint`
[Section titled “POST /api/admin/ai/routes/{route\_id}/repoint”](#post-apiadminairoutesroute_idrepoint)
Repoint Inference Route
Move everything pinned to this route onto another one, atomically.
The bulk repoint, and **the only supported way a model changes route**. All four stores move in one transaction — catalog rows, prompt templates, mode presets, feature-model settings — because a partial repoint is worse than none: a mode preset left naming a route that is then disabled takes down titles, summaries, extraction and compaction simultaneously.
Afterwards the source route disables cleanly — **unless an org has its own offering on it**. Those are moved by nobody but the org: they are the record of where that tenant’s traffic goes and what it costs, so an admin tidying routes must not relocate them as a side effect. `GET …/pins` reports them separately and answers `repoint_clears: false` when they are there, which is the case where running this action first would be wasted.
Refusals, all 422 except the missing target:
* the target does not exist (404) or is the source itself;
* the target is **disabled** — repointing onto a route nothing may dispatch through just relocates the outage;
* the target’s **dialect cannot serve** some row’s `kind`. Checked per row through `_ensure_dialect_matches_kind`, the same predicate `create` and the per-row repoint go through, because “a row lands on a route” has to have exactly one choke point — a chat model on `voyage-direct` would otherwise be reachable in bulk while being refused one at a time.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| route\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------- | --------------- | -------- | ----------- |
| target\_route\_id | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `RepointRouteResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/models`
[Section titled “GET /api/admin/models”](#get-apiadminmodels)
List Catalog Models
Every catalog row, enabled or not, in display order.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `ModelCatalogEntryResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/models`
[Section titled “POST /api/admin/models”](#post-apiadminmodels)
Create Catalog Model
Add a model on a route. `(key, route)` must be unique.
A key may appear on two routes — that is the point of the pair — but only one of them may be enabled at a time, so the conflict below covers both the same-pair collision and the second-enabled-row one.
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------------------- | -------------------- | -------- | ----------- |
| allowed\_efforts | `string[]` | no | |
| cache\_write\_price\_per\_1m | `number` \| `string` | no | |
| cached\_input\_price\_per\_1m | `number` \| `string` | no | |
| context\_window | `integer` | no | |
| cost\_tier | `string` | no | |
| enabled | `boolean` | no | |
| fallback\_credits\_per\_1k\_tokens | `number` \| `string` | no | |
| input\_price\_per\_1m | `number` \| `string` | no | |
| key | `string` | yes | |
| kind | `string` | no | |
| label | `string` | yes | |
| max\_output\_tokens | `integer` | no | |
| output\_price\_per\_1m | `number` \| `string` | no | |
| provider | `string` | yes | |
| remote\_model\_id | `string` | no | |
| replacement\_model\_id | `string (uuid)` | no | |
| route\_id | `string (uuid)` | yes | |
| sort\_order | `integer` | no | |
| supports\_reasoning | `boolean` | no | |
| supports\_vision | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 201 | Successful Response | `ModelCatalogEntryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/admin/models/{entry_id}`
[Section titled “PATCH /api/admin/models/{entry\_id}”](#patch-apiadminmodelsentry_id)
Update Catalog Model
Update a model. Switching `enabled` off takes effect platform-wide.
Runs already pinned to a disabled model are not rewritten: they resolve to `replacement_model_id` (or the platform default) lazily at dispatch.
Renaming `key` is refused with **409** while any live config still names the old one, listing what to repoint first — a rename is not a relabel, it changes the id sent on the wire and every consumer stores that id by value. Nothing cascades and no historical row is touched. To follow a vendor’s own id change, add a second row and point the old one’s `replacement_model_id` at it; that is what lazy retirement is for.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| entry\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------------------- | -------------------- | -------- | ----------- |
| allowed\_efforts | `string[]` | no | |
| cache\_write\_price\_per\_1m | `number` \| `string` | no | |
| cached\_input\_price\_per\_1m | `number` \| `string` | no | |
| context\_window | `integer` | no | |
| cost\_tier | `string` | no | |
| deprecated | `boolean` | no | |
| enabled | `boolean` | no | |
| fallback\_credits\_per\_1k\_tokens | `number` \| `string` | no | |
| input\_price\_per\_1m | `number` \| `string` | no | |
| key | `string` | no | |
| kind | `string` | no | |
| label | `string` | no | |
| max\_output\_tokens | `integer` | no | |
| output\_price\_per\_1m | `number` \| `string` | no | |
| provider | `string` | no | |
| remote\_model\_id | `string` | no | |
| replacement\_model\_id | `string (uuid)` | no | |
| route\_id | `string (uuid)` | no | |
| sort\_order | `integer` | no | |
| supports\_reasoning | `boolean` | no | |
| supports\_vision | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `ModelCatalogEntryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/admin/models/{entry_id}`
[Section titled “DELETE /api/admin/models/{entry\_id}”](#delete-apiadminmodelsentry_id)
Delete Catalog Model
Remove a model from the catalog.
Prefer disabling: a deleted row loses its replacement pointer, so anything still pinned to the key falls all the way through to the platform default.
**Refused with 409 while anything still names the key.** That advice was only ever a docstring, and the failure it warned about is worse than “falls through to the default”: `ensure_model_known` is deliberately lenient about a *retired* model (the row stays, so already-pinned agents keep deploying) and has no way to be lenient about one that is simply gone. A delete therefore turns every agent pinned to the key unshippable, and for a credit-plan org — whose plan forbids it from naming a model at all — unfixable from the UI. Retiring the row (`enabled=false` plus a `replacement_model_id`) is the operation this endpoint’s callers almost always want, and the message says so.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| entry\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/models/{entry_id}/sync-price`
[Section titled “POST /api/admin/models/{entry\_id}/sync-price”](#post-apiadminmodelsentry_idsync-price)
Sync Catalog Model Price
Re-price one model from the community LiteLLM dataset.
A convenience for identifying prices, not a runtime dependency: runs bill from the catalog, and this is one way to fill it. Deliberate per-row action, so it *does* overwrite what’s there — the boot-time sweep is the one that only fills blanks, precisely so a hand-typed price survives it.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| entry\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `ModelCatalogEntryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs/{org_id}/ai-settings`
[Section titled “GET /api/admin/orgs/{org\_id}/ai-settings”](#get-apiadminorgsorg_idai-settings)
Get Org Ai Settings
One org’s runtime overrides, plus the platform values behind its nulls.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `OrgRuntimeSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/admin/orgs/{org_id}/ai-settings`
[Section titled “PATCH /api/admin/orgs/{org\_id}/ai-settings”](#patch-apiadminorgsorg_idai-settings)
Update Org Ai Settings
Set (or clear) one org’s runtime overrides on its behalf.
Writes the same rows the org’s own settings page writes, so support flipping a single org and that org flipping itself are the same operation — and the platform tier stays where everyone else reads it.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------- | --------- | -------- | ----------- |
| compaction\_enabled | `boolean` | no | |
| compaction\_threshold | `number` | no | |
| decision\_log\_enabled | `boolean` | no | |
| thinking\_enabled | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `OrgRuntimeSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Admin Autopin
> REST API reference for admin autopin.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/admin/autopin-settings`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/admin/autopin-settings`
[Section titled “GET /api/admin/autopin-settings”](#get-apiadminautopin-settings)
Get Autopin Settings
The auto-pin policy as the sweep will read it tonight.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `AutoPinSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/admin/autopin-settings`
[Section titled “PATCH /api/admin/autopin-settings”](#patch-apiadminautopin-settings)
Update Autopin Settings
Update the auto-pin knobs; switching the feature off clears its pins.
**Request body** (required)
| Field | Type | Required | Description |
| --------------- | --------- | -------- | ----------- |
| enabled | `boolean` | no | |
| keep\_coverage | `number` | no | |
| max\_per\_agent | `integer` | no | |
| min\_calls | `integer` | no | |
| min\_chats | `integer` | no | |
| min\_coverage | `number` | no | |
| window\_days | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `AutoPinSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Admin Billing
> REST API reference for admin billing.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/admin/billing/catalog`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/admin/billing/catalog`
[Section titled “GET /api/admin/billing/catalog”](#get-apiadminbillingcatalog)
Get Billing Catalog
Every plan and pack with its Polar product mapping, plus collisions.
`null` on a product id is a state, not a gap: it means the row is not sellable through Polar, which is correct for `trial` and `enterprise` and is what lets the checkout API refuse them without a special-case list.
`collisions` is what the database cannot enforce. It is advisory — the webhook resolver is the authority and its answer to an ambiguous product id is `unresolved`, i.e. a payment that lands nowhere until somebody fixes the mapping here.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `BillingCatalogResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/billing/events`
[Section titled “GET /api/admin/billing/events”](#get-apiadminbillingevents)
List Billing Events
The webhook ingress log, newest first, payloads included.
`status_counts` covers the whole table rather than the page, because the question this screen answers is “is any money stranded”, and a page of `applied` rows says nothing about the `unresolved` one behind it.
Payloads are returned whole. That is the point of storing them: when a customer says “I paid and got nothing”, this plus `credit_ledger` is the entire answer, and Polar’s own delivery log is not queryable from our side.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | --------------------------------------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | query | `string (uuid)` | no | |
| status | query | `string` | no | Filter to one BillingEventStatus value. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `BillingEventListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/billing/events/{event_id}/replay`
[Section titled “POST /api/admin/billing/events/{event\_id}/replay”](#post-apiadminbillingeventsevent_idreplay)
Replay Billing Event
Re-run a stored delivery’s effect.
**Re-runs the effect, not the status stamp.** Marking a row `applied` without moving any money would turn a stranded payment into an invisible one, which is strictly worse than leaving it stranded. Re-applying is safe because every writer underneath is idempotent on its own deterministic ledger key.
Refuses an `applied` row: that status is the only thing a redelivery is allowed to short-circuit on, and re-running it would be the one case the ledger keys are not protecting against a *second* human decision.
The effect itself lives in the webhook ingress. When that is not in the build this answers 503 naming the symbol it looked for, and touches nothing — no status change, no `attempts` bump — so the row stays exactly as diagnosable as it was.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| event\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `BillingEventReplayResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/credit-packs`
[Section titled “GET /api/admin/credit-packs”](#get-apiadmincredit-packs)
List Credit Packs
Every pack, active or retired, in display order.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `PackProductRow[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/credit-packs`
[Section titled “POST /api/admin/credit-packs”](#post-apiadmincredit-packs)
Create Credit Pack
Create a credit pack. `code` must be unique.
**Request body** (required)
| Field | Type | Required | Description |
| ------------------ | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| code | `string` | yes | |
| credits | `number` \| `string` | yes | Credits granted on payment. Must be positive and representable in NUMERIC(20, 6) — a pack worth zero credits is a mistake, not a state anyone chose. |
| is\_active | `boolean` | no | |
| name | `string` | yes | |
| polar\_product\_id | `string (uuid)` | no | |
| price\_currency | `string` | no | ISO 4217 alpha-3 the price is quoted in. Set it to whatever the Polar product is priced in — the figure above carries no currency of its own, and every surface that renders it reads this. |
| price\_display | `number` \| `string` | no | Display only. The price actually charged is the one on the Polar product; nothing bills from this column. |
| sort\_order | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `PackProductRow` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/admin/credit-packs/{pack_id}`
[Section titled “PUT /api/admin/credit-packs/{pack\_id}”](#put-apiadmincredit-packspack_id)
Update Credit Pack
Replace a pack. Complete payload, like a plan write.
Editing `credits` does not retro-adjust anyone: a purchase already made wrote its own ledger row for the amount that was in force at the time, and the ledger is append-only. This changes what the *next* buyer gets.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| pack\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------------ | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| code | `string` | yes | |
| credits | `number` \| `string` | yes | Credits granted on payment. Must be positive and representable in NUMERIC(20, 6) — a pack worth zero credits is a mistake, not a state anyone chose. |
| is\_active | `boolean` | no | |
| name | `string` | yes | |
| polar\_product\_id | `string (uuid)` | no | |
| price\_currency | `string` | no | ISO 4217 alpha-3 the price is quoted in. Set it to whatever the Polar product is priced in — the figure above carries no currency of its own, and every surface that renders it reads this. |
| price\_display | `number` \| `string` | no | Display only. The price actually charged is the one on the Polar product; nothing bills from this column. |
| sort\_order | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `PackProductRow` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/admin/credit-packs/{pack_id}`
[Section titled “DELETE /api/admin/credit-packs/{pack\_id}”](#delete-apiadmincredit-packspack_id)
Delete Credit Pack
Delete a pack.
Deactivating (`is_active=false`) is nearly always the right move instead: a retired pack that stays in the table keeps an old purchase explainable, and a deleted one leaves a paid `order.paid` replay with no product to resolve against. Deletion stays available for a pack created by mistake and never sold.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| pack\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs/{org_id}/billing`
[Section titled “GET /api/admin/orgs/{org\_id}/billing”](#get-apiadminorgsorg_idbilling)
Get Org Billing
This org’s external subscription, Polar’s verbatim status, and its link.
Two status columns, on purpose. `external_status` is what Polar said; `status` is what our period job and credit gates read. Conflating them would let a Polar state we have never heard of silently mean “active” or silently mean “blocked”, and both are wrong — so they are shown side by side and a disagreement is something a human can see.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgBillingResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/admin/plans/{plan_id}/polar-products`
[Section titled “PUT /api/admin/plans/{plan\_id}/polar-products”](#put-apiadminplansplan_idpolar-products)
Set Plan Polar Products
Register (or clear) a plan’s Polar product ids.
Sending `null` is a deliberate write meaning “not sellable”, not an omission — which is why this is its own endpoint rather than two more fields on the plan editor’s total-replace payload.
A duplicate id comes back as 409 rather than a 500: the partial unique index is the guard that stops two plans resolving one product ambiguously, and an operator hitting it deserves to be told which guard they hit.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| plan\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------------------------- | --------------- | -------- | ----------- |
| polar\_product\_id | `string (uuid)` | no | |
| polar\_product\_id\_annual | `string (uuid)` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `BillingCatalogResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Admin Costs
> REST API reference for admin costs.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/admin/cogs-rates`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/admin/cogs-rates`
[Section titled “GET /api/admin/cogs-rates”](#get-apiadmincogs-rates)
List Cogs Rates
Current plan + this month’s consumption for each flat-rate provider.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CogsRateResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/admin/cogs-rates/{metric}`
[Section titled “PUT /api/admin/cogs-rates/{metric}”](#put-apiadmincogs-ratesmetric)
Update Cogs Rate
Set a provider’s plan, then re-price the affected rollups.
`effective_from` defaults to the start of the current month so the change covers the month being billed. Anything already rolled up from that date forward is recomputed, since the hourly cron would only ever revisit its own trailing window.
**Parameters**
| Name | In | Type | Required | Description |
| ------ | ---- | -------- | -------- | ----------- |
| metric | path | `string` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------------ | -------------------- | -------- | ----------- |
| billing\_mode | `string` | no | |
| effective\_from | `string (date)` | no | |
| included\_units | `integer` | no | |
| monthly\_fee\_usd | `number` \| `string` | no | |
| overage\_unit\_usd | `number` \| `string` | no | |
| plan | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CogsRateResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Admin Credits
> REST API reference for admin credits.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `PUT https://api.agentdepot.org/api/admin/credit-display-fx`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### PUT `/api/admin/credit-display-fx`
[Section titled “PUT /api/admin/credit-display-fx”](#put-apiadmincredit-display-fx)
Put Display Fx
Set the display-only FX rates, replacing whatever was there.
Deliberately **not** on the rate card. The card is versioned because burns cite it; this is a rendering aid whose only effect is the number an operator reads off a screen. Versioning it would imply a burn could be reconstructed differently depending on the rate in force, which is exactly the confusion worth avoiding — no burn has ever touched an exchange rate.
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| rates | `object` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `CreditRateCardResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/credit-rate-card`
[Section titled “GET /api/admin/credit-rate-card”](#get-apiadmincredit-rate-card)
Get Credit Rate Card
Every published version of the card, plus the live margin readout.
`margins` is what makes `credits_per_usd` safe to edit: it is one number that reprices the whole product, and the only way to see what it does is against the price points the plans actually sell at.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `CreditRateCardResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/admin/credit-rate-card`
[Section titled “PUT /api/admin/credit-rate-card”](#put-apiadmincredit-rate-card)
Publish Credit Rate Card
Publish a **new** card version. Existing versions are never mutated.
Appends to the stored history rather than replacing it, so every burn receipt that cites an older `card_version` still reconstructs to the same number and re-deriving history from the ledger keeps reconciling (plan §3.1, §13.1 scenario 11). `with_card` replaces by `effective_from`, so re-publishing the same date is a correction of an unstarted version rather than a second entry — publishing a *change* means a new date.
**Request body** (required)
| Field | Type | Required | Description |
| ----------------- | -------------------- | -------- | ----------- |
| credits\_per\_usd | `number` \| `string` | yes | |
| effective\_from | `string (date)` | yes | |
| rules | `CreditRuleInput[]` | no | |
| version | `integer` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `CreditRateCardResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/mode-presets/preview`
[Section titled “POST /api/admin/mode-presets/preview”](#post-apiadminmode-presetspreview)
Preview Mode Presets
Burn ratios the submitted presets *would* produce, without saving them.
Plan §7.4: re-pointing `mode_normal` at a dearer model changes every credit org’s burn rate at once, and because `normal` is the denominator of every ratio, re-pointing it rescales all of them. The save path in `admin_ai.py` already writes its own audit row with the resulting ratios; what was missing is seeing them *before* committing, which is this.
Read-only, so it deliberately writes no audit row — the save does.
**Request body** (required)
| Field | Type | Required | Description |
| ------------------ | -------- | -------- | ----------- |
| mode\_high\_effort | `object` | no | |
| mode\_normal | `object` | no | |
| mode\_trivial | `object` | no | |
| mode\_x\_high | `object` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `ModePresetPreviewResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs/{org_id}/credits`
[Section titled “GET /api/admin/orgs/{org\_id}/credits”](#get-apiadminorgsorg_idcredits)
Get Org Credits
Balance, subscription and resolved entitlements for one org.
`reconciled` is the invariant the whole ledger design rests on: the cached balance — **both buckets summed** — must equal the sum of every ledger row. False means something wrote `credit_balances` without a ledger row, which is a bug worth chasing before believing any other number on the screen.
Do not read `purchase_total` as the target for `purchased_balance`. It is what the org has bought; the bucket is what is left of it after burns spilled into it, and the two are equal only for an org that has bought and not spent.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgCreditsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/orgs/{org_id}/credits/adjust`
[Section titled “POST /api/admin/orgs/{org\_id}/credits/adjust”](#post-apiadminorgsorg_idcreditsadjust)
Adjust Org Credits
Move an org’s balance by a signed delta, with a mandatory reason.
Writes **two** records, and both matter: a `credit_ledger` row carrying the actor and the reason (the money, and what keeps `balance == SUM(delta)` true) and an `admin_audit_log` row (the admin action). Plan §13.1 scenario 12 asserts both — a mutating admin call with no audit row is a finding regardless of whether the ledger is right.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------ | -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| delta | `number` \| `string` | yes | Signed movement in credits. Negative debits the org. |
| reason | `string` | yes | Why this adjustment was made. Mandatory: it lands on both the ledger row and the audit row, and an unexplained manual movement is indistinguishable from a bug later. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `AdjustCreditsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/orgs/{org_id}/credits/forensics`
[Section titled “GET /api/admin/orgs/{org\_id}/credits/forensics”](#get-apiadminorgsorg_idcreditsforensics)
Org Credit Forensics
Burn drill-down for one org, plus the reconciliation check.
The buckets are windowed; `ledger_total` and `reconciled` are not. That split is deliberate — a drill-down answers “where did this month go”, while reconciliation is an all-time property (`balance + purchased_balance` must equal the sum of *every* row), and computing it over a window would make it pass by accident. Plan §13.1 scenario 12 asserts the view sums to the ledger.
Every bucket carries dollars alongside credits, read from the burn receipts rather than divided back out of the credits. The two are not the same question — credits are what the org was charged, dollars are what the usage cost us — and having both on one row is what makes a multiplier’s effect visible without opening the model catalog.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ------------------------ |
| days | query | `integer` | no | Window size, ending now. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ForensicsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/orgs/{org_id}/reset-baseline`
[Section titled “POST /api/admin/orgs/{org\_id}/reset-baseline”](#post-apiadminorgsorg_idreset-baseline)
Reset Org Baseline
Put an org back on a known plan, balance, floor, overrides and period.
Orgs cannot be deleted, so this **is** teardown for the credits e2e sweep’s persistent fixture orgs (plan §13.1). Without it they drift and a later run passes for the wrong reason — a scenario asserting “the gate refuses” trivially succeeds against an org somebody left at zero.
The balance lands via an `adjustment`, never an `UPDATE credit_balances`: a hand-written balance write leaves `balance != SUM(credit_ledger.delta)` and silently disables the reconciliation check every later run depends on.
Idempotent in the sense that matters — the second call finds the org already at baseline, computes a zero delta and writes no ledger row. It is not “no writes at all”: the ledger is append-only, so a reset that *does* move something adds to it rather than erasing history.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | -------- | -------- | ----------- |
| plan\_code | `string` | yes | |
| reason | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `ResetBaselineResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/orgs/{org_id}/roll-period`
[Section titled “POST /api/admin/orgs/{org\_id}/roll-period”](#post-apiadminorgsorg_idroll-period)
Roll Org Period
Run the period close for one org, optionally forcing the boundary.
Two very different operations behind one endpoint:
* **Un-forced** is just running the scheduled job early, narrowed to this org. If the period has not ended the org is not due and nothing happens, so it is safe anywhere and needs no gate.
* **Forced** pulls `period_end` back to now first, which expires a live balance and issues the next grant ahead of schedule. That is real money moving on a schedule nobody agreed to, so it is refused unless `CREDITS_FORCE_PERIOD_ROLL_ENABLED` is set — **non-prod only**, and default-deny so a fresh deployment is production-shaped. Staging turns it on for plan §13.1 scenarios 5 and 8, which cannot otherwise observe a rollover without waiting a month.
The close itself is the period job, unchanged — it opens its own session per org and commits there, so this handler commits its own work first.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| force | `boolean` | no | Pull period\_end back to now first, so the close runs even though the period has not ended. Non-prod only — this expires a live balance and grants early. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `RollPeriodResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/admin/orgs/{org_id}/subscription`
[Section titled “PUT /api/admin/orgs/{org\_id}/subscription”](#put-apiadminorgsorg_idsubscription)
Update Org Subscription
Change an org’s plan, its entitlement overrides, or its period boundary.
A plan change goes through `change_plan`, which expires the outgoing balance and grants the new plan’s allocation — once ever per org per plan, so an org returning to a plan it has already had gets nothing and must be topped up with an adjustment instead.
Overrides are tri-state on the wire: a field omitted from `overrides` is left as it is, an explicit `null` clears it back to inheriting the plan, and a value sets it. Sending `overrides: {}` therefore changes nothing — clearing them all means naming them all, or using `reset-baseline`.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------- | ---------------------- | -------- | ----------- |
| override\_reason | `string` | no | |
| overrides | `EntitlementOverrides` | no | |
| period\_end | `string (date-time)` | no | |
| plan\_code | `string` | no | |
| status | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgCreditsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/plans`
[Section titled “GET /api/admin/plans”](#get-apiadminplans)
List Plans
Every plan, with its price point, margin and subscriber count.
The `fields` list is the entitlement set, derived from `ENTITLEMENT_FIELDS`. The editor renders from it rather than from a hand-written list, so a new entitlement cannot exist on the server and be missing from the screen that is supposed to set it.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `PlanListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/admin/plans`
[Section titled “POST /api/admin/plans”](#post-apiadminplans)
Create Plan
Create a plan. `code` must be unique.
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| audit\_log | `boolean` | no | |
| byo\_keys | `boolean` | no | |
| code | `string` | yes | |
| decision\_log | `boolean` | no | |
| decision\_retention\_days | `integer` | no | |
| direct\_model\_choice | `boolean` | no | |
| enforced\_idp\_sso | `boolean` | no | |
| enforces\_credits | `boolean` | no | |
| hitl\_routing | `boolean` | no | |
| initial\_credits | `number` \| `string` | no | |
| intake\_classifier\_calls\_per\_day | `integer` | no | |
| intake\_messages\_per\_day | `integer` | no | |
| is\_enterprise | `boolean` | no | |
| max\_agents | `integer` | no | |
| max\_concurrent\_chats | `integer` | no | |
| max\_custom\_environments | `integer` | no | |
| max\_custom\_image\_gb | `integer` | no | |
| max\_custom\_models | `integer` | no | |
| max\_inboxes | `integer` | no | |
| max\_integrations | `integer` | no | |
| max\_queue\_depth | `integer` | no | |
| max\_seats | `integer` | no | |
| monthly\_credits | `number` \| `string` | no | |
| name | `string` | yes | |
| overdraft\_pct | `number` \| `string` | no | |
| per\_agent\_budgets | `boolean` | no | |
| price\_currency | `string` | no | ISO 4217 alpha-3 the advertised price is quoted in. Set it to whatever the plan’s Polar product is priced in. |
| price\_display | `number` \| `string` | no | |
| renews | `boolean` | no | |
| shared\_blocks | `boolean` | no | |
| sort\_order | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `PlanResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/admin/plans/{plan_id}`
[Section titled “PUT /api/admin/plans/{plan\_id}”](#put-apiadminplansplan_id)
Update Plan
Replace a plan’s figures and entitlements.
Takes effect on the **next** entitlement read: `EntitlementService` caches per instance and an instance lives for one request, so no running request is retroactively re-permissioned and the one after this sees the new values.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| plan\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| audit\_log | `boolean` | no | |
| byo\_keys | `boolean` | no | |
| code | `string` | yes | |
| decision\_log | `boolean` | no | |
| decision\_retention\_days | `integer` | no | |
| direct\_model\_choice | `boolean` | no | |
| enforced\_idp\_sso | `boolean` | no | |
| enforces\_credits | `boolean` | no | |
| hitl\_routing | `boolean` | no | |
| initial\_credits | `number` \| `string` | no | |
| intake\_classifier\_calls\_per\_day | `integer` | no | |
| intake\_messages\_per\_day | `integer` | no | |
| is\_enterprise | `boolean` | no | |
| max\_agents | `integer` | no | |
| max\_concurrent\_chats | `integer` | no | |
| max\_custom\_environments | `integer` | no | |
| max\_custom\_image\_gb | `integer` | no | |
| max\_custom\_models | `integer` | no | |
| max\_inboxes | `integer` | no | |
| max\_integrations | `integer` | no | |
| max\_queue\_depth | `integer` | no | |
| max\_seats | `integer` | no | |
| monthly\_credits | `number` \| `string` | no | |
| name | `string` | yes | |
| overdraft\_pct | `number` \| `string` | no | |
| per\_agent\_budgets | `boolean` | no | |
| price\_currency | `string` | no | ISO 4217 alpha-3 the advertised price is quoted in. Set it to whatever the plan’s Polar product is priced in. |
| price\_display | `number` \| `string` | no | |
| renews | `boolean` | no | |
| shared\_blocks | `boolean` | no | |
| sort\_order | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `PlanResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/admin/plans/{plan_id}`
[Section titled “DELETE /api/admin/plans/{plan\_id}”](#delete-apiadminplansplan_id)
Delete Plan
Delete a plan. Refused while any org is subscribed to it.
409 rather than a cascade: the subscription’s `plan_id` is what every entitlement read resolves through, so removing the plan under a live subscriber turns that org `UNSUBSCRIBED` — everything denied — from a click that looked like tidying up.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| plan\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# Admin Experiments
> REST API reference for admin experiments.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/admin/agent-experiments-settings`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/admin/agent-experiments-settings`
[Section titled “GET /api/admin/agent-experiments-settings”](#get-apiadminagent-experiments-settings)
Get Agent Experiments Settings
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------- |
| 200 | Successful Response | `AgentExperimentsSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/admin/agent-experiments-settings`
[Section titled “PATCH /api/admin/agent-experiments-settings”](#patch-apiadminagent-experiments-settings)
Update Agent Experiments Settings
**Request body** (required)
| Field | Type | Required | Description |
| ------- | --------- | -------- | ----------- |
| enabled | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------- |
| 200 | Successful Response | `AgentExperimentsSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Admin Users
> REST API reference for admin users.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/admin/users`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/admin/users`
[Section titled “GET /api/admin/users”](#get-apiadminusers)
List Users
List/search every user on the platform, cross-tenant.
Org counts and platform-admin flags come from `public`; the sign-in columns are a second pass over Better Auth’s tables, joined by email and omitted entirely when that schema is unreadable.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ----- | --------------- | -------- | ----------------------------------------------- |
| admins\_only | query | `boolean` | no | Only users holding platform-admin access. |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | query | `string (uuid)` | no | Only members of this organization. |
| q | query | `string` | no | Search email/name (substring, case-insensitive) |
| status | query | `string` | no | Filter on the account’s `is_active` flag. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `UserListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/users/{user_id}`
[Section titled “GET /api/admin/users/{user\_id}”](#get-apiadminusersuser_id)
Get User
One user: account, org memberships, API tokens, and sign-in identity.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| user\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `UserDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/users/{user_id}/audit`
[Section titled “GET /api/admin/users/{user\_id}/audit”](#get-apiadminusersuser_idaudit)
Get User Audit
Admin-panel actions this user performed, plus actions performed on them.
Three shapes have to be matched, because `admin_audit_log` records a user in three different places: as the `actor`, as a `user`-typed target (deactivate, admin grant), and — for membership changes — as `meta.user_id` behind an `org_membership` target id. Matching only the first two silently drops every “added to org X” row, which is exactly the history a support question asks about.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| user\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `UserAuditResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/admin/users/{user_id}/sessions`
[Section titled “GET /api/admin/users/{user\_id}/sessions”](#get-apiadminusersuser_idsessions)
Get User Sessions
Sign-in history: the Better Auth sessions still on record for this user.
Empty with `auth_directory_available: false` means “we cannot see the auth tables”, which is a different statement from “this user never signed in” — the console distinguishes the two.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| user\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `SignInHistoryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Agent Chats
> REST API reference for agent chats.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agents/{agent_id}/chats/`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/”](#get-apiorgsorg_idagentsagent_idchats)
List Chats
List chat conversations for an agent (no message content — summary only).
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/”](#post-apiorgsorg_idagentsagent_idchats)
Create Chat
Create a new chat conversation with an agent.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| environment\_id | `string (uuid)` | no | |
| model\_mode\_override | `string` | no | Per-chat effort level (‘trivial’ \| ‘normal’ \| ‘high\_effort’ \| ‘x\_high’), beating the agent’s own `model_mode` for this chat only. Only meaningful for an org whose plan selects effort rather than models: setting it on an org with direct model choice is a 422, because the value would be silently inert. Null inherits the agent’s level. See `GET /api/orgs/{org_id}/model-modes` for the available levels, their labels and their credit burn ratios. |
| sandbox\_enabled | `boolean` | no | |
| title | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `ChatResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}”](#get-apiorgsorg_idagentsagent_idchatschat_id)
Get Chat
Get a chat conversation with all messages.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}`
[Section titled “PATCH /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}”](#patch-apiorgsorg_idagentsagent_idchatschat_id)
Update Chat
Update a chat — title, archived state, and/or shared visibility.
At least one of `title`, `archived` or `shared` must be provided. `shared` (private↔shared visibility) is creator-only.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------- | --------- | -------- | ----------- |
| archived | `boolean` | no | |
| shared | `boolean` | no | |
| title | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}`
[Section titled “DELETE /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}”](#delete-apiorgsorg_idagentsagent_idchatschat_id)
Delete Chat
Soft-delete a chat conversation.
Deleting closes the chat as surely as `/terminate` does — more so, since the row stops being reachable at all — so it resolves any pending HITL request to `cancelled` and invalidates unfinished todos the same way. A request left pending here can never be answered (its chat 404s) and never be cleared, so it sits in the HITL inbox forever.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/continue`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/continue”](#post-apiorgsorg_idagentsagent_idchatschat_idcontinue)
Continue Chat
Carry this chat on — same chat, same transcript, next turn.
Two shapes of stuck chat land here, and the difference is what the button says, never what this endpoint does:
* **Continue** — a liftable blocker stopped it: the org ran out of credits, or the model it runs on had no price to charge credits against (`CONTINUABLE_STOP_REASONS`). Both ARCHIVE the chat, so it sits there with everything the agent had already done and no way to reach it.
* **Run again** — the last turn simply *failed* (`ERRORED`: a provider that refused, a stream that died). Nothing archived it and its composer still works, so until this endpoint accepted it the only way to carry the chat on was to type a message like “continue” — inventing a user turn that says nothing, purely to trigger the dispatch. That message is also not free: it changes `trigger_message_id`, which keys the checkpointer thread, so the failed turn’s committed state is abandoned and its tool work re-done. Dispatching with no new message resumes it instead.
`/retry` is neither: a retry is a *clone* that starts the work over, discarding the transcript this endpoint exists to keep.
Deliberately a button rather than something a top-up does by itself. “Money arrived” is not “and I still want all forty of those turns to run”: some of the blocked work will have been overtaken by events, and re-running it costs real credits. So the person who paid picks the chats that still matter.
**The blocker is re-checked here, not trusted from the client.** A tab that has been open since before the balance emptied still shows the button, and dispatching on its say-so would burn a worker slot to stop the chat again two seconds later — with a second identical notice in the transcript. An org that is still out of credits gets the same 402 every other ingress does.
A model that is still unpriced cannot be re-checked as cheaply (it needs the chat’s resolved model *and* route, which only the runtime knows), so that one is allowed through: the turn refuses at the gate exactly as before and the chat lands back where it was, one wasted admission later.
Un-archiving is done here and nowhere else. `dispatch_conversation_turn` revives PAUSED and ERRORED chats — the states a fresh user message revives — and ARCHIVED is deliberately not in that set: it is the terminal a user reaches with `/terminate`, and a message must never quietly reopen it.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/cost-breakdown`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/cost-breakdown”](#get-apiorgsorg_idagentsagent_idchatschat_idcost-breakdown)
Get Chat Cost Breakdown
Per-token-type cost attribution for this chat’s LLM spend.
Split out from `GET /{chat_id}` rather than inlined on `ChatResponse`: the popover that shows this is opened on demand, and every other chat surface (list, sidebar, detail) has no use for a per-line breakdown on every row. See `chat_cost_breakdown` for the attribution rules — in short, totals are always the stored `cost_usd` (never recomputed from rates), and a credits org gets the same shape with every dollar field null, same as everywhere else on this API (`docs/api/spend-display.md`).
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `ChatCostBreakdownResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/messages`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/messages”](#post-apiorgsorg_idagentsagent_idchatschat_idmessages)
Send Message
Send a user message and dispatch the agent turn to the worker.
Persists the user message immediately (for optimistic echo + sidebar bump), then dispatches a `CONVERSATION_TURN_REQUESTED` event to the worker. The worker owns LLM execution, assistant-message persistence, cost rollup, and title generation.
Returns 202 Accepted with the persisted user message id. Returns 503 if the worker dispatch fails (a silent 202 would leave the turn never running).
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------------------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| attachment\_ids | `string (uuid)[]` | no | |
| content | `string` | yes | |
| model | `string` | no | |
| model\_mode\_override | `string` | no | Per-chat effort level (‘trivial’ \| ‘normal’ \| ‘high\_effort’ \| ‘x\_high’), beating the agent’s own `model_mode` for this chat only. Only meaningful for an org whose plan selects effort rather than models: setting it on an org with direct model choice is a 422, because the value would be silently inert. Null inherits the agent’s level. See `GET /api/orgs/{org_id}/model-modes` for the available levels, their labels and their credit burn ratios. |
| reasoning\_effort | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 202 | Successful Response | `SendMessageResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/presence`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/presence”](#post-apiorgsorg_idagentsagent_idchatschat_idpresence)
Heartbeat Presence
Say “I am looking at this chat” and get back everyone else who is.
Called on an interval by an open chat tab. The roster is live Redis state: a viewer who stops calling ages out on its own, so a crashed browser or a closed laptop cannot leave a ghost in the header.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `ChatPresenceResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/presence`
[Section titled “DELETE /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/presence”](#delete-apiorgsorg_idagentsagent_idchatschat_idpresence)
Leave Presence
Leave the roster immediately, rather than waiting to age out.
Best effort — the stale cutoff is what actually guarantees the roster stays honest, since a closing tab is not a reliable narrator.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `ChatPresenceResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/retry`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/retry”](#post-apiorgsorg_idagentsagent_idchatschat_idretry)
Retry Chat
Re-run this chat’s inputs from scratch, in a brand new chat.
A retry is a *clone*, never a resume: the original is left exactly as it is (so a failed run stays readable) and a fresh chat is created carrying the same opening user turns, the same attachments, the same originator context and the same per-chat runtime choices — then dispatched immediately.
The agent’s own configuration (instruction, tools, skills, team context) is re-frozen from its CURRENT state rather than copied from the source chat’s snapshots: fixing the agent and pressing retry is the point of the button.
Returns 201 with the new chat, whose id the caller opens. 503 if the worker dispatch fails — the clone exists but would never run, so it must not read as success.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `ChatResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/stop`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/stop”](#post-apiorgsorg_idagentsagent_idchatschat_idstop)
Stop Chat
Stop this chat’s work — keep the chat itself alive.
A RUNNING chat flips to ACTIVE so the in-flight worker aborts at its next LLM step (`pre_llm_checks` raises `AssignmentCancelledError` whenever `chat.status != RUNNING`), and `stop_requested_at` is stamped so a turn that was dispatched but had not yet started is skipped at entry instead of flipping itself back to RUNNING.
A chat that is merely *waiting* is stopped too, and this is the case that matters most: a parent parked on the children it spawned (`awaiting_children`), a chat asleep on its own timer, or one queued behind the org’s concurrency cap is not running, but it is going to start again by itself. Its pause is cleared and the machinery that would resume it — the open child-chat wait, any pending timer — is torn down. A chat paused on a *human* keeps its pause: stopping the work must not discard the question somebody was asked.
With `cascade` the same treatment is applied to every chat descended from this one, to any depth and across agents — an agent that fanned work out to children otherwise leaves them all running (and spending) when the chat the user is watching stops. Each stopped chat’s open child-chat wait is abandoned in the same transaction, so a child settling a moment later cannot wake the subtree back up.
Unlike `/terminate` the chat is **not** closed: no `completed_at`, no `archived_at`, and no HITL resolution. The chat stays live and the user can immediately send another message.
Idempotent: if the chat is not RUNNING (already ACTIVE, PAUSED, or in a terminal state) 200 is returned without mutation — stopping a non-running chat is always a no-op.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body**: `StopChatRequest`
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `StopChatResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/terminate`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/terminate”](#post-apiorgsorg_idagentsagent_idchatschat_idterminate)
Terminate Chat
Terminate a chat — set status to ARCHIVED and resolve any pending HITL.
Flips `chat.status` to `ARCHIVED` (the user-closed terminal state) and sets `completed_at` to now. The ORM listener fires a `status_changed` realtime envelope so the frontend receives the update immediately.
Any pending HITL request for this chat is resolved to `cancelled` so the chat is not left awaiting human input.
Idempotent: if the chat is already in a terminal status (ERRORED, ARCHIVED, ABANDONED) 200 is returned without re-mutating.
Setting status away from RUNNING signals an in-flight runner to abort at its next LLM step (`pre_llm_checks` raises `AssignmentCancelledError`). No extra worker call is needed.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `TerminateChatResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/timeline`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/timeline”](#get-apiorgsorg_idagentsagent_idchatschat_idtimeline)
Get Conversation Timeline
Get the unified conversation-spine timeline for a chat.
Returns a newest-first list of events composed from:
1. All `agent_events` for this `chat_id` — regardless of whether each row has `assignment_id` set or NULL. This captures `agent_step` prose rows written by a chat-lane turn before or without span attribution, which the per-assignment endpoint misses.
2. Synthetic `user_message_received` events derived from `agent_chat_messages WHERE role='user'` for this chat. User messages are not on the event spine, but the frontend renders the whole conversation from this single feed, so user turns are synthesised here. `metadata.content` carries the message text; `metadata.attachments` carries any file attachments so the frontend can render file chips.
The merged list is sorted newest-first (same convention as `GET /assignments/{id}/timeline`) so the frontend `buildTurns` (which does `[...events].reverse()`) works unchanged.
Pagination: `limit` / `offset` are applied AFTER the merge-sort, so the total reflects the combined event + user-message count.
Requires membership in the organization and that the chat belongs to the given agent.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | -------------------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| limit | query | `integer` | no | Max events to return |
| offset | query | `integer` | no | Pagination offset |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `ConversationTimelineResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/toggles`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/toggles”](#get-apiorgsorg_idagentsagent_idchatschat_idtoggles)
Get Chat Toggles
Get the available composer toggles for a chat with their current enabled state.
Always includes a `system:web` entry for the web-search bundle. Also includes one entry per ACTIVE Composio integration connected to this org.
The `enabled` field reflects whether the scope is currently active for the chat (i.e. is present in `enabled_scopes`).
Only connections **this agent can reach** are offered — see :func:`_build_toggle_items`. Only Composio integrations are surfaced here — MCP-server and other integration types are not toggle-able via this surface.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `TogglesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/toggles`
[Section titled “PUT /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/toggles”](#put-apiorgsorg_idagentsagent_idchatschat_idtoggles)
Update Chat Toggles
Update the composer toggle state for a chat.
Replaces `enabled_scopes` with the provided list. Every scope in the request body must be one of the allowed values for this chat (`system:web` or a `composio:` scope for an ACTIVE Composio integration this *agent* can reach) — anything else is rejected with 422. Enabling a scope the agent cannot resolve is refused rather than stored: unlike an agent’s `allowed_tools`, a chat toggle names a connection that exists now and is acted on this turn, so there is no “granted before it existed” ordering to protect and a stored one could only ever be a lie.
Returns the updated toggle list (same shape as GET /{chat\_id}/toggles).
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------------- | ---------- | -------- | ----------- |
| enabled\_scopes | `string[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `TogglesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/archive-batch`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/archive-batch”](#post-apiorgsorg_idagentsagent_idchatsarchive-batch)
Archive Chats
Archive (or unarchive) a selection of chats — the All chats table’s other bulk action, beside `continue-batch`.
**Archiving here also closes what is still open.** A table row is archived so it stops cluttering the list, and the rows that clutter it are the finished ones — but “select all” is a rough gesture and a running chat is one row away from a done one. Hiding a chat that is still spending credits is the one outcome nobody asked for and nobody would notice, so a chat in a non-terminal status (ACTIVE / RUNNING / PAUSED) is terminated first: status → ARCHIVED, `completed_at` set, pending HITL requests cancelled and unfinished todos invalidated, exactly the steps `POST /{chat_id}/terminate` performs. Only then is `archived_at` set.
A chat already in a terminal status (ERRORED / ARCHIVED / ABANDONED) keeps its lifecycle untouched — `terminate_chat` is idempotent on those — so an *errored* chat stays errored and stays rerunnable, and unarchiving it later hands back a chat the table can still offer Continue on. That distinction is the whole reason terminate and archive are separate columns, and it is what keeps this from being a bulk delete.
**This is deliberately more than the sidebar’s archive toggle**, which is hide-only (`PATCH /{chat_id}` with `archived=true`). The two are not interchangeable and the difference is stated in the UI’s confirm dialog: a single chat can be tidied away without being ended, while a bulk gesture big enough to hide a dozen chats is also big enough to need to stop them.
Unarchiving is the same call with `archived=false`: it clears `archived_at` and touches no lifecycle state, which is right because the archived view has nothing running in it to stop.
**Partial success is the contract, not a fallback** — same as `continue-batch`, and for the same reason: a selection is a rough gesture over a filtered table, and some rows will have moved on, been deleted, or belong to another agent. Every chat is attempted and reported on individually, in the order it was asked for.
Sequential on purpose, one commit per chat, so an error partway leaves the chats before it archived rather than rolling them back.
No credit gate and no suspension gate: archiving spends nothing, and an org that has run out must still be able to tidy up.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | ----------------- | -------- | ----------- |
| archived | `boolean` | no | |
| chat\_ids | `string (uuid)[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `ArchiveChatsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/browse`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/browse”](#get-apiorgsorg_idagentsagent_idchatsbrowse)
Browse Chats
The All chats table: the sidebar’s rows, filtered, sorted and paged.
Deliberately separate from `/unified` in signature and identical in row shape. The sidebar is a keyset feed of recent activity and must stay that way (it appends as you scroll); a table needs page numbers, a total, and sorts that reorder the whole set, which keyset cannot express. Both read :func:`_chat_list_select`, so the two can’t disagree about a row.
`status` and `resolution` are separate filters because they answer different questions: status is “is this chat moving?”, resolution is “how did the work come out?”. Folding them into one filter is what made “finished, but a human still has to fix it” unfindable.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ----- | ---------------------------------------- | -------- | ---------------------------------------------------------------------- |
| agent\_id | path | `string (uuid)` | yes | |
| archived | query | `boolean` | no | True returns archived chats only; false (default) returns active ones. |
| from | query | `string (date-time)` | no | Only chats whose last activity is at or after this. |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| order | query | `"asc"` \| `"desc"` | no | |
| org\_id | path | `string (uuid)` | yes | |
| q | query | `string` | no | Case-insensitive substring match on the chat title. |
| resolution | query | `string[]` | no | Repeatable ChatResolution filter. |
| revision | query | `integer` | no | Agent revision version the chat runs on (pinned or inherited). |
| sort | query | `"activity"` \| `"cost"` \| `"messages"` | no | |
| source | query | `string[]` | no | Repeatable ChatSource filter. |
| status | query | `string[]` | no | Repeatable ChatStatus filter. |
| to | query | `string (date-time)` | no | Only chats whose last activity is at or before this. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatBrowseResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/continue-batch`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/continue-batch”](#post-apiorgsorg_idagentsagent_idchatscontinue-batch)
Continue Chats
Continue a selection of chats — the All chats table’s bulk action.
One provider outage or one exhausted balance strands chats by the dozen, all at once and all for the same reason. Clearing that by opening each chat and pressing the same button is the actual cost of the incident, and it is why the table exists.
**Partial success is the contract, not a fallback.** A selection is a rough gesture over a filtered table: some rows will have moved on, been archived, or already been continued from another tab. Failing the whole call because one of thirty is no longer continuable would make the feature unusable for exactly the mess it is for. Every chat is attempted and reported on individually, in the order it was asked for, and each one that started is genuinely dispatched — this returns no optimistic rows.
Sequential on purpose. Each chat commits its own revival before the next is touched, so an error partway leaves the ones before it running rather than rolling them back, and the credit gate is re-read per chat: a batch that empties the balance halfway must stop spending, not discover it at the thirtieth worker.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | ----------------- | -------- | ----------- |
| chat\_ids | `string (uuid)[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `ContinueChatsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/unified`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/unified”](#get-apiorgsorg_idagentsagent_idchatsunified)
List Unified Sidebar
Unified sidebar list: conversations ordered by activity.
Phase 2 (chat-outcomes model): chatless assignment items are gone — every work unit is a chat.
Returns a keyset-paginated list of conversations for the agent sidebar.
When `archived=true`, only archived conversations are returned. The response includes `archived_count=0` on this branch.
When `archived=false` (default), only active (non-archived) conversations are returned. The response includes `archived_count` with the total number of archived conversations for this agent.
Keyset pagination: cursor encodes (activity\_at ISO, id) as base64. Use next\_cursor from the response to fetch the next page.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| agent\_id | path | `string (uuid)` | yes | |
| archived | query | `boolean` | no | When true, return archived conversations only; when false (default), return active (non-archived) conversations and chatless assignments. |
| cursor | query | `string` | no | Opaque keyset cursor |
| limit | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `UnifiedSidebarResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Agent Memories
> REST API reference for agent memories.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agent-memories`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agent-memories`
[Section titled “GET /api/orgs/{org\_id}/agent-memories”](#get-apiorgsorg_idagent-memories)
List Agent Memories
List agent memories, optionally filtered by agent.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | --------------- |
| agent\_id | query | `string (uuid)` | no | Filter by agent |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `AgentMemoryListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agent-memories/search`
[Section titled “GET /api/orgs/{org\_id}/agent-memories/search”](#get-apiorgsorg_idagent-memoriessearch)
Search Agent Memories
Vector search agent memories by semantic similarity.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------------- |
| agent\_id | query | `string (uuid)` | no | Filter by agent |
| limit | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| q | query | `string` | no | Search query text |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `AgentMemorySearchResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Agent Rollouts
> REST API reference for agent rollouts.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agents/{agent_id}/rollouts`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agents/{agent_id}/rollouts`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/rollouts”](#get-apiorgsorg_idagentsagent_idrollouts)
List Rollouts
List every rollout (deploys and experiments) an agent has ever had, newest first.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `RolloutListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/rollouts`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/rollouts”](#post-apiorgsorg_idagentsagent_idrollouts)
Start Experiment
Start a 2-5 arm A/B experiment, superseding whatever is currently running.
Requires ADMIN or OWNER role, and the platform’s `agent_experiments_enabled` setting to be on (403 otherwise — a platform admin turns it on). Every arm must resolve to a revision belonging to this agent (by version, id, or `use_draft` for the current draft — sealed on start), pass the org’s model allowlist, and have every tool/skill/knowledge-base grant resolve in the org’s catalog and the agent’s own reach — the same checks `PATCH /agents/{id}` runs, just for every arm instead of one config. Weights must sum to 10 000 (basis points); `control` must name one of the arm labels.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------- | ------------------------ | -------- | ----------- |
| allow\_identical | `boolean` | no | |
| arms | `ExperimentArmRequest[]` | yes | |
| control | `string` | yes | |
| hypothesis | `string` | no | |
| label | `string` | no | |
| unit | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 201 | Successful Response | `RolloutDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/rollouts/{rollout_id}`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/rollouts/{rollout\_id}”](#get-apiorgsorg_idagentsagent_idrolloutsrollout_id)
Get Rollout
One rollout’s arms (label, revision version, weight) and live per-arm chat counts.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| rollout\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `RolloutDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/agents/{agent_id}/rollouts/{rollout_id}`
[Section titled “PATCH /api/orgs/{org\_id}/agents/{agent\_id}/rollouts/{rollout\_id}”](#patch-apiorgsorg_idagentsagent_idrolloutsrollout_id)
Update Rollout
Rename an experiment and/or edit its hypothesis.
Requires ADMIN or OWNER role. Partial: only the fields actually present in the body change (`null` or `""` clears them); the rollout must be an experiment, not a plain deploy (409).
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| rollout\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | -------- | -------- | ----------- |
| hypothesis | `string` | no | |
| label | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `RolloutDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/rollouts/{rollout_id}/promote`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/rollouts/{rollout\_id}/promote”](#post-apiorgsorg_idagentsagent_idrolloutsrollout_idpromote)
Promote Experiment Arm
End the running experiment as promoted and deploy the named arm at 100%.
`rollout_id` must be the *currently running* experiment — 409 otherwise (naming what is actually running, if anything). Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| rollout\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| arm | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `RolloutDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/rollouts/{rollout_id}/results`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/rollouts/{rollout\_id}/results”](#get-apiorgsorg_idagentsagent_idrolloutsrollout_idresults)
Get Rollout Results
Per-arm results for one rollout: outcome distribution, cost, tokens, tool-call and HITL rates, and duration — each with a 95% confidence interval — plus each non-control arm’s delta vs the control arm and a `significant` flag (intervals that don’t overlap). `insufficient_data` on an arm means its `counted` chats are below the rollout’s `min_sample`; every figure is still computed, just worth caveating in the UI.
Cost is reported in exactly one of `credits`/`usd` per `billing_unit` — never both, matching every other spend-carrying response in this API.
Cached for about 45 seconds per rollout, so a page polling this while an experiment runs will see the same numbers for short bursts rather than re-running the full aggregation on every request.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| rollout\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `RolloutResultsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/rollouts/{rollout_id}/stop`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/rollouts/{rollout\_id}/stop”](#post-apiorgsorg_idagentsagent_idrolloutsrollout_idstop)
Stop Experiment
End the running experiment as stopped and deploy the control revision at 100%.
`rollout_id` must be the currently running experiment. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| rollout\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `RolloutDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Agent Teams
> REST API reference for agent teams.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agent-teams`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agent-teams`
[Section titled “GET /api/orgs/{org\_id}/agent-teams”](#get-apiorgsorg_idagent-teams)
List Teams
List all agent teams in the organization. Requires membership.
Omit `limit` to get every team; `total` is the full count either way.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_TeamResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agent-teams`
[Section titled “POST /api/orgs/{org\_id}/agent-teams”](#post-apiorgsorg_idagent-teams)
Create Team
Create a new agent team. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| avatar\_seed | `string` | no | |
| description | `string` | no | |
| name | `string` | yes | |
| shared\_allowed\_tools | `string[]` | no | |
| shared\_instruction | `string` | no | |
| shared\_mounted\_skills | `string[]` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `TeamResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agent-teams/{team_id}`
[Section titled “GET /api/orgs/{org\_id}/agent-teams/{team\_id}”](#get-apiorgsorg_idagent-teamsteam_id)
Get Team
Get an agent team by UUID or slug. Requires membership.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| team\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `TeamResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/agent-teams/{team_id}`
[Section titled “PATCH /api/orgs/{org\_id}/agent-teams/{team\_id}”](#patch-apiorgsorg_idagent-teamsteam_id)
Update Team
Update an agent team. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| team\_id | path | `string` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| avatar\_seed | `string` | no | |
| description | `string` | no | |
| name | `string` | no | |
| shared\_allowed\_tools | `string[]` | no | |
| shared\_instruction | `string` | no | |
| shared\_mounted\_skills | `string[]` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `TeamResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/agent-teams/{team_id}`
[Section titled “DELETE /api/orgs/{org\_id}/agent-teams/{team\_id}”](#delete-apiorgsorg_idagent-teamsteam_id)
Delete Team
Delete an agent team. Agents become ungrouped. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| team\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agent-teams/{team_id}/memories`
[Section titled “GET /api/orgs/{org\_id}/agent-teams/{team\_id}/memories”](#get-apiorgsorg_idagent-teamsteam_idmemories)
List Team Memories
List memories owned by this team. Requires org membership.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| team\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `TeamMemoryListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agent-teams/{team_id}/memories`
[Section titled “POST /api/orgs/{org\_id}/agent-teams/{team\_id}/memories”](#post-apiorgsorg_idagent-teamsteam_idmemories)
Create Team Memory
Create a memory owned by this team. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| team\_id | path | `string` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------- | -------- | -------- | ----------- |
| content | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `AgentMemoryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/agent-teams/{team_id}/memories/{memory_id}`
[Section titled “DELETE /api/orgs/{org\_id}/agent-teams/{team\_id}/memories/{memory\_id}”](#delete-apiorgsorg_idagent-teamsteam_idmemoriesmemory_id)
Delete Team Memory
Delete a team memory. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| memory\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| team\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/agent-teams/reorder`
[Section titled “PUT /api/orgs/{org\_id}/agent-teams/reorder”](#put-apiorgsorg_idagent-teamsreorder)
Reorder Teams
Reorder teams by setting display\_order. Requires membership.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | ----------------- | -------- | ----------- |
| team\_ids | `string (uuid)[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# Agent Toolkit
> REST API reference for agent toolkit.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agent-toolkit`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agent-toolkit`
[Section titled “GET /api/orgs/{org\_id}/agent-toolkit”](#get-apiorgsorg_idagent-toolkit)
Get Agent Toolkit
Return the complete toolkit inventory for an org.
Combines agents, prompt templates (which may carry an extraction schema), custom tools, connected (MCP/system/native-integration) tools, skills, and processes in a single response.
When `agent_slug` is provided the agent must belong to `org_id` (404 otherwise). `connected_tools` is then filtered to only those tools whose name appears in the agent’s `allowed_tools`. If `allowed_tools` is `["*"]`, the full list is returned.
All other lists are always unfiltered.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ----------------------------------------------------------------- |
| agent\_slug | query | `string` | no | When set, filter connected\_tools to this agent’s allowed\_tools. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `AgentToolkitResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Agents
> REST API reference for agents.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agents`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agents`
[Section titled “GET /api/orgs/{org\_id}/agents”](#get-apiorgsorg_idagents)
List Agents
List agents in the organization. Requires membership.
Omit `limit` to get every agent; `total` is the full count either way.
Every filter and the sort are applied **inside the statement**, before `LIMIT`. So the returned page is a genuine page of the filtered, ordered set, and `total` is the count of that filtered set — not of the org. A caller must not re-filter or re-sort the page it gets back: doing so over one loaded page is what this signature exists to stop.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ----- | ----------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| deployed | query | `boolean` | no | true returns only agents with an active revision, false only those without one. Omit for both. Mirrors `has_active_revision` on the row. |
| execution\_mode | query | `AgentExecutionMode` | no | Filter by execution mode. Omit to get every mode. |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | query | `string` | no | Filter by project id, or ‘none’ for uncategorized agents. |
| q | query | `string` | no | Case-insensitive substring match over agent name, slug and description. Blank or whitespace-only is treated as absent. |
| sort | query | `"display_order"` \| `"name"` \| `"created_at"` \| `"last_activity_at"` | no | Sort key. `display_order` (default) is the org’s manual arrangement. `last_activity_at` is the agent’s most recent chat activity of all time — never-run agents sort last in both directions. Window-scoped rankings (runs / failures / spend over 24h-30d) are not offered here; `GET /agents/activity` answers those for every agent, unpaginated. |
| sort\_order | query | `"asc"` \| `"desc"` | no | Sort direction. Omit for the natural direction of the chosen sort: asc for `display_order` and `name`, desc for `created_at` and `last_activity_at`. |
| status | query | `AgentStatus` | no | Filter by lifecycle status. Omit to get both active and archived. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_AgentResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents`
[Section titled “POST /api/orgs/{org\_id}/agents”](#post-apiorgsorg_idagents)
Create Agent
Create a new agent in the organization. Requires ADMIN or OWNER role.
The agent’s v1 revision starts as an undeployed draft (`has_undeployed_draft: true` in the response) — it cannot run a chat until deployed via `POST /agents/{id}/deploy`. Until then the returned agent fields mirror the v1 draft, so the detail page shows the values just submitted.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| allowed\_knowledge\_bases | `string[]` | no | |
| allowed\_tools | `string[]` | no | |
| always\_forward\_chat\_files | `boolean` | no | |
| auto\_pin\_enabled | `boolean` | no | |
| avatar\_seed | `string` | no | |
| can\_manage\_integrations | `boolean` | no | |
| can\_remember | `boolean` | no | |
| can\_request\_human\_input | `boolean` | no | |
| decision\_log\_enabled | `boolean` | no | |
| description | `string` | no | |
| environment\_id | `string (uuid)` | no | |
| execution\_mode | `string` | no | |
| instruction | `string` | no | |
| model | `string` | no | |
| model\_mode | `string` | no | Effort level for an org whose plan selects effort rather than models: ‘trivial’ (Trivial), ‘normal’ (Standard), ‘high\_effort’ (High) or ‘x\_high’ (X-High). The platform decides what each level runs (model + reasoning effort + thinking). No level is plan-gated. Rejected with 422 for an org whose plan has direct model choice, where a level would have no effect — such an org sets `model`/`reasoning_effort` instead. Null clears it. |
| mounted\_skills | `string[]` | no | |
| name | `string` | yes | |
| outcome\_schema | `object` | no | |
| per\_chat\_cost\_limit\_usd | `number` | no | |
| per\_chat\_credit\_limit | `number` | no | |
| permissions | `object` | no | |
| pinned\_tools | `string[]` | no | |
| reasoning\_effort | `string` | no | |
| reply\_to\_incoming\_email | `boolean` | no | |
| sandbox\_enabled | `boolean` | no | |
| status | `string` | no | |
| tags | `string[]` | no | |
| team\_id | `string (uuid)` | no | |
| vision\_model | `string` | no | Optional override for the model this agent uses to describe images and scanned pages it opens with `read_file` (the platform’s “Image and PDF reading” feature model). Null (the default) defers to this org’s own override of that feature, if it has set one, then to the platform’s. Allowed values are the same models this agent could set as `model` — governed by the same platform/org allowlist chain — narrowed to those whose catalog row supports vision; a model that cannot read images is rejected with 422. Null clears it. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `AgentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}”](#get-apiorgsorg_idagentsagent_id)
Get Agent
Get agent details. Accepts UUID or slug. Requires membership.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AgentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/agents/{agent_id}`
[Section titled “PATCH /api/orgs/{org\_id}/agents/{agent\_id}”](#patch-apiorgsorg_idagentsagent_id)
Update Agent
Update agent settings. Accepts UUID or slug.
Every behaviour field — `instruction`, `model`, `reasoning_effort`, `model_mode`, `vision_model`, `allowed_tools`, `allowed_knowledge_bases`, `mounted_skills`, `pinned_tools`, `outcome_schema`, and the capability flags — is routed to the agent’s draft revision (forked from the active one if it has none) and does not take effect until deployed. Pass `deploy: true` to deploy it in this same request, or call `POST /agents/{id}/deploy` afterwards; a 409 means the draft would be identical to what is already active. Every other field (name, description, status, execution\_mode, team, budgets, tags, …) applies immediately either way.
Narrowing `allowed_tools` also unpins: any `pinned_tools` entry the new grants no longer admit is dropped, because a pin is scope-unioned into the effective allow-set at run time and would otherwise survive the revocation.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| allowed\_knowledge\_bases | `string[]` | no | |
| allowed\_tools | `string[]` | no | |
| always\_forward\_chat\_files | `boolean` | no | |
| auto\_pin\_enabled | `boolean` | no | |
| auto\_pinned\_tools | `string[]` | no | |
| avatar\_seed | `string` | no | |
| can\_manage\_integrations | `boolean` | no | |
| can\_remember | `boolean` | no | |
| can\_request\_human\_input | `boolean` | no | |
| decision\_log\_enabled | `boolean` | no | |
| deploy | `boolean` | no | |
| description | `string` | no | |
| environment\_id | `string (uuid)` | no | |
| execution\_mode | `string` | no | |
| instruction | `string` | no | |
| model | `string` | no | |
| model\_mode | `string` | no | Effort level for an org whose plan selects effort rather than models: ‘trivial’ (Trivial), ‘normal’ (Standard), ‘high\_effort’ (High) or ‘x\_high’ (X-High). The platform decides what each level runs (model + reasoning effort + thinking). No level is plan-gated. Rejected with 422 for an org whose plan has direct model choice, where a level would have no effect — such an org sets `model`/`reasoning_effort` instead. Null clears it. |
| mounted\_skills | `string[]` | no | |
| name | `string` | no | |
| outcome\_schema | `object` | no | |
| owner\_user\_id | `string (uuid)` | no | |
| per\_chat\_cost\_limit\_usd | `number` | no | |
| per\_chat\_credit\_limit | `number` | no | |
| permissions | `object` | no | |
| pinned\_tools | `string[]` | no | |
| reasoning\_effort | `string` | no | |
| reply\_to\_incoming\_email | `boolean` | no | |
| run\_as | `string` | no | |
| sandbox\_enabled | `boolean` | no | |
| settings | `object` | no | |
| status | `string` | no | |
| tags | `string[]` | no | |
| team\_id | `string (uuid)` | no | |
| tools | `object[]` | no | |
| vision\_model | `string` | no | Optional override for the model this agent uses to describe images and scanned pages it opens with `read_file` (the platform’s “Image and PDF reading” feature model). Null (the default) defers to this org’s own override of that feature, if it has set one, then to the platform’s. Allowed values are the same models this agent could set as `model` — governed by the same platform/org allowlist chain — narrowed to those whose catalog row supports vision; a model that cannot read images is rejected with 422. Null clears it. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AgentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/agents/{agent_id}`
[Section titled “DELETE /api/orgs/{org\_id}/agents/{agent\_id}”](#delete-apiorgsorg_idagentsagent_id)
Delete Agent
Soft-delete an agent. Accepts UUID or slug. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/artifacts`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/artifacts”](#get-apiorgsorg_idagentsagent_idartifacts)
Get Agent Artifacts
Get artifacts produced by an agent.
Accepts UUID or slug. Returns artifacts ordered by created\_at desc. Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `AgentArtifactListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/deploy`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/deploy”](#post-apiorgsorg_idagentsagent_iddeploy)
Deploy Agent
Deploy the draft revision: seal it, make it active, and mirror its full behavioural config (instruction, model, tools, skills, outcome schema, capability flags — everything `PATCH` can route to a draft) onto the agent’s live-reading columns.
Accepts UUID or slug. Requires ADMIN or OWNER role. 409 if the draft is identical to the revision already active — sealing it would mint a version nothing distinguishes.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AgentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/draft`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/draft”](#post-apiorgsorg_idagentsagent_iddraft)
Create Draft
Start (or reset) the draft from a chosen revision’s full config.
The rollback recipe: the UI’s “Restore as draft” only copies instruction text; this copies the **entire** behavioural config — model, effort, mode, tools, knowledge bases, mounted skills, pins, outcome schema, every capability flag — from `from_version` (or, if omitted, from the currently active revision) onto the draft. Follow with `POST /agents/{id}/deploy` to make it live.
If the agent has no draft, one is created (forking a new version). If it already has a draft, it is overwritten *in place* (same id/version) — unless it carries unsaved changes of its own, in which case this is refused with a 409 naming how many fields would be discarded; pass `replace: true` to overwrite anyway. 404 if `from_version` does not name a live revision on this agent (or, when omitted, if the agent has never been deployed).
Legal while an experiment is running — drafts are always fine to create; only deploying one is refused during a rollout.
Accepts UUID or slug. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------- | --------- | -------- | ----------- |
| from\_version | `integer` | no | |
| replace | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AgentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/agents/{agent_id}/draft`
[Section titled “DELETE /api/orgs/{org\_id}/agents/{agent\_id}/draft”](#delete-apiorgsorg_idagentsagent_iddraft)
Discard Draft
Discard the draft revision for an agent.
Accepts UUID or slug. Requires ADMIN or OWNER role. Returns 422 if no draft exists.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AgentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/events`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/events”](#get-apiorgsorg_idagentsagent_idevents)
Get Agent Events
Get the activity timeline for an agent.
Accepts UUID or slug. Returns events ordered by created\_at desc (newest first). Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | -------------------- |
| agent\_id | path | `string` | yes | |
| limit | query | `integer` | no | Max events to return |
| offset | query | `integer` | no | Pagination offset |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `AgentEventTimelineResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/revisions`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/revisions”](#get-apiorgsorg_idagentsagent_idrevisions)
List Revisions
List all revisions for an agent, newest first.
Accepts UUID or slug. Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `AgentRevisionListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/activity`
[Section titled “GET /api/orgs/{org\_id}/agents/activity”](#get-apiorgsorg_idagentsactivity)
Get Agents Activity
Current state, sparkline and window totals for every agent in the org.
One entry per agent — including agents that have never run — so a grid can key straight off this without deciding what a missing row meant. The three facts a caller needs to tell “loading” from “never ran” from “quiet lately” are `last_activity_at` (null = never), `ran_in_window`, and the totals; see `services/agent_activity.py`.
Money comes back in one unit, named by `spend_unit`: a BYO org reads `cost_usd`, everyone else reads `credits`.
Deliberately not paginated. It is bounded by the org’s agent count, it backs a page that draws all of them at once, and a paged sparkline would just be N round trips wearing a different hat.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | -------------------------------------------- |
| org\_id | path | `string (uuid)` | yes | |
| window | query | `string` | no | Window ending now. One of 24h, 7d, 14d, 30d. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `AgentsActivityResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/deleted`
[Section titled “GET /api/orgs/{org\_id}/agents/deleted”](#get-apiorgsorg_idagentsdeleted)
List Deleted Agents
List soft-deleted agents in the organization. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_AgentResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
# Alerts
> REST API reference for alerts.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/alerts`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/alerts`
[Section titled “GET /api/orgs/{org\_id}/alerts”](#get-apiorgsorg_idalerts)
List Alerts
Page the org’s alert feed, newest occurrence first.
Private alerts (raised on a chat that is not shared) are visible only to their owner, mirroring the rule the recent-chats list already applies.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | ------------------- | -------- | --------------------- |
| org\_id | path | `string (uuid)` | yes | |
| page | query | `integer` | no | |
| per\_page | query | `integer` | no | |
| status | query | `"open"` \| `"all"` | no | open (default) or all |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AlertListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/alerts/{alert_id}/read`
[Section titled “POST /api/orgs/{org\_id}/alerts/{alert\_id}/read”](#post-apiorgsorg_idalertsalert_idread)
Mark Alert Read
Mark one alert read **for the calling user only**.
Does not resolve it: the alert stays in every member’s list, this one included, until somebody actually deals with it.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| alert\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `AlertMutationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/alerts/{alert_id}/resolve`
[Section titled “POST /api/orgs/{org\_id}/alerts/{alert\_id}/resolve”](#post-apiorgsorg_idalertsalert_idresolve)
Resolve Alert
Close one alert **for the whole org**.
This is the affordance the old surface never had: an errored chat could only leave the list by being revived, so the count only ever climbed. Resolving is shared on purpose — the thing has been dealt with, so it should not still be demanding attention from four other people.
Idempotent: resolving an already-resolved alert keeps the original resolution and attribution rather than rewriting who dealt with it.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| alert\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `AlertMutationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/alerts/{alert_id}/unread`
[Section titled “POST /api/orgs/{org\_id}/alerts/{alert\_id}/unread”](#post-apiorgsorg_idalertsalert_idunread)
Mark Alert Unread
Undo :func:`mark_alert_read` for the calling user.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| alert\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `AlertMutationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/alerts/read-all`
[Section titled “POST /api/orgs/{org\_id}/alerts/read-all”](#post-apiorgsorg_idalertsread-all)
Mark All Alerts Read
Clear the calling user’s unread emphasis across the whole open feed.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `AlertMutationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/alerts/resolve-all`
[Section titled “POST /api/orgs/{org\_id}/alerts/resolve-all”](#post-apiorgsorg_idalertsresolve-all)
Resolve All Alerts
Close every open alert the caller can see, for the whole org.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `AlertMutationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Artifacts
> REST API reference for artifacts.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/artifacts`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/artifacts`
[Section titled “GET /api/orgs/{org\_id}/artifacts”](#get-apiorgsorg_idartifacts)
List Artifacts
List artifacts in the organization, newest first.
Mirrors the detail endpoint’s scoping: org membership is sufficient, since `org_id` is a direct column on every artifact — this surfaces exactly what `GET /{artifact_id}` already lets a member read one at a time, it does not widen access.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `Page_ArtifactDetailResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/artifacts/{artifact_id}`
[Section titled “GET /api/orgs/{org\_id}/artifacts/{artifact\_id}”](#get-apiorgsorg_idartifactsartifact_id)
Get Artifact
Get a single artifact by ID.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| artifact\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `ArtifactDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/artifacts/{artifact_id}/download`
[Section titled “GET /api/orgs/{org\_id}/artifacts/{artifact\_id}/download”](#get-apiorgsorg_idartifactsartifact_iddownload)
Download Artifact
Download the raw bytes of a FILE-type artifact.
Only valid for artifacts with artifact\_type == “file”. For all other types (code, markdown, text, json) the content is already inline in the `GET /{artifact_id}` response.
Returns the file bytes with appropriate Content-Type and Content-Disposition headers so browsers trigger a native download.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| artifact\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
# Authentication
> REST API reference for authentication.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/auth/me`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/auth/me`
[Section titled “GET /api/auth/me”](#get-apiauthme)
Get Me
Get identity info for the authenticated principal.
Works for both user tokens (JWT / agd\_\* user token) and agent tokens.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `PrincipalResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Billing
> REST API reference for billing.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/billing`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/billing`
[Section titled “GET /api/orgs/{org\_id}/billing”](#get-apiorgsorg_idbilling)
Get Billing
The org’s billing state, plus what it can buy.
**This is the endpoint the UI is driven by, including immediately after a checkout.** Polar’s success redirect and Polar’s webhook race each other, and both orders happen; the redirect carries a checkout id for support, and the page polls here until `subscription.plan_code` is the plan that was bought. Reading the plan off the redirect would show a customer a plan they have not been provisioned for, or a stale one after they have.
`plans` and `packs` list only what is **sellable** — a Polar product id is registered for it. That is why `trial` and `enterprise` never appear here, with no list of plan codes anywhere in this module to keep in sync.
Member-level: a member sees the page and gets `can_manage = false`, so the buttons can be disabled with a reason instead of the page 403-ing.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `BillingResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/billing/cancellation`
[Section titled “POST /api/orgs/{org\_id}/billing/cancellation”](#post-apiorgsorg_idbillingcancellation)
Set Cancellation
Cancel at the end of the paid period, or undo that.
One endpoint with a boolean rather than `/cancel` and `/uncancel`, because it sets one field on Polar’s side and a pair of verbs would be two routes that can disagree about which one won.
**Not a revocation and not an entitlement change.** The customer paid for this period and keeps every capability in it; what changes is whether it renews. When it finally ends, Polar sends `subscription.revoked` and the webhook lands the org on `trial` with its purchased credits intact (§6.4).
Like every other route here it **writes nothing**. Our `cancel_at_period_end` column is written by the `subscription.canceled` / `subscription.uncanceled` delivery, so this returns what Polar answered and `GET /billing` catches up within seconds. A second writer for that flag would be the same mistake as a second writer for the plan, with lower stakes and no upside.
Unlike a plan switch this has **no pending marker**. A switch has just taken the customer’s money and leaves every visible number stale until the webhook lands, which is worth a column; a cancellation moves no money, changes no entitlement and no balance, so a stale flag for a few seconds costs a caller who ignores this response nothing at all.
OWNER or ADMIN only — it decides whether the org keeps paying.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | --------- | -------- | ----------- |
| cancel\_at\_period\_end | `boolean` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `CancellationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/billing/change-plan`
[Section titled “POST /api/orgs/{org\_id}/billing/change-plan”](#post-apiorgsorg_idbillingchange-plan)
Change Billing Plan
Switch a live subscription onto another plan, billed pro rata now.
**This endpoint applies nothing.** It calls Polar and returns. Our `plan_id`, the entitlements it resolves and the credit movement all happen when a Polar delivery confirms the switch.
That is not caution, it is the only correct shape. The grant key is scoped by an identifier for *this* switch, precisely so a customer returning to a plan they have held before (`pro → team → pro`) is granted for it rather than refused by a once-ever key. An endpoint that moved the plan itself would leave the confirming delivery arriving at an already-changed plan, where `change_plan` short-circuits — and the identifier would never reach a grant key at all. The customer would pay and receive nothing, silently. That is the defect #497 fixed and #502’s handlers are built around.
What it *does* write is a marker naming the plan asked for and a `change_id`. It does two jobs:
* `GET /billing` reports the gap, so a customer who has just been charged a proration does not see a page where nothing has changed;
* it is what lets a **downgrade** land at all. A downgrade’s proration is a *credit*, not a charge, so it may never produce an `order.paid` — and that event is the sole writer of a paid plan change. A subscription event is allowed to confirm the switch this marker names, keyed on its `change_id`, which keeps one logical writer with two possible confirmations. See `services/billing/plan_change.py`.
Refusals:
* unknown `plan_code` → **404**;
* a plan with no Polar product → **409** (`trial`, `enterprise`);
* the plan the org is already on → **409**; Polar would bill a proration of nothing and our `change_plan` short-circuits anyway;
* no live external subscription → **409**, with checkout as the answer;
* the same switch already in flight → **409**, so a double-click cannot buy two prorations. A switch to a *different* plan is allowed and replaces the marker: a customer changing their mind is not an error;
* Polar refusing (a currency change, an already-cancelled subscription) → **422** carrying its reason; Polar unavailable → **502**.
OWNER or ADMIN only.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | -------- | -------- | ------------------------------- |
| plan\_code | `string` | yes | Plan to switch to, e.g. `team`. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChangePlanResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/billing/checkout`
[Section titled “POST /api/orgs/{org\_id}/billing/checkout”](#post-apiorgsorg_idbillingcheckout)
Create Checkout
Open a Polar checkout for one plan or one credit pack.
**Returning a URL means the customer has somewhere to pay, nothing more.** No plan is changed, no credit is granted and nothing is written to `org_subscriptions` here. All of that happens when the webhook lands, so that a checkout the customer abandons — or one Polar later rejects — leaves no trace on the org.
Refusals, and why each is the status it is:
* unknown `plan_code` / `pack_code` → **404**, it does not exist;
* a plan or pack with no Polar product registered → **409**, it exists and is deliberately not for sale (`trial`, `enterprise`, a pack an operator has not finished registering);
* a plan checkout while the org already has a live Polar subscription → **409**, because a second subscription for one org collides with `ux_org_subscriptions_external_sub` *after* the card is charged. Switching plans is a different operation with a different endpoint.
Pack checkouts are never refused for having a subscription: a top-up is a one-off purchase and an org may buy as many as it likes.
**The buyer’s details are prefilled, and one person may buy for many orgs.** The Polar customer is the *payer*; the org this purchase is for travels in the checkout metadata, which Polar copies onto the order and the subscription. So a second org checked out by the same person reuses their billing account — no second email to invent, nothing retyped — and the delivery still resolves to the org named here. See :func:`~agentdepot_core.services.billing.polar_client.create_checkout_session`.
OWNER or ADMIN only.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | -------- | -------- | ------------------------------------------- |
| pack\_code | `string` | no | One-off credit pack to buy, e.g. `pack_1k`. |
| plan\_code | `string` | no | Plan to subscribe to, e.g. `pro`. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CheckoutResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/billing/portal`
[Section titled “POST /api/orgs/{org\_id}/billing/portal”](#post-apiorgsorg_idbillingportal)
Create Portal
Mint a fresh customer-portal session for the org’s billing account.
Invoices, payment method and cancellation. **Not plan changes** — those are disabled on the Polar side, because a switch made in the portal produces no order id, and the grant key that keeps one payment from being applied twice is keyed on the order id. A customer who switched there would pay and receive nothing.
The token is short-lived, so this is called **on the click** and the URL is never prefetched or cached. An org that has never checked out has no Polar customer, which is answered from our own column without troubling Polar — a state to explain, not an error.
⚠️ **The portal belongs to the payer, not to this org.** Its scope is the Polar customer, and under the payer model one person’s customer bills for every org they bought for — so this link shows that person’s other orgs’ invoices and subscriptions too, and can cancel them. That is inherent in letting one card buy for many orgs; an org wanting its billing walled off needs a different payer. OWNER or ADMIN only, which is the only gate there is on it.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `PortalResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Chat Files
> REST API reference for chat files.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/files/`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/files/`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/files/”](#get-apiorgsorg_idagentsagent_idchatschat_idfiles)
List Chat Files
List all files attached to a chat conversation.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `FileListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/files/`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/files/”](#post-apiorgsorg_idagentsagent_idchatschat_idfiles)
Attach Chat Files
Attach previously uploaded temp files to a chat conversation.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | ------------------------------------------------ | -------- | ----------- |
| file\_refs | `agentdepot_api__routers__chat_files__FileRef[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `FileListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/files/{file_id}`
[Section titled “DELETE /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/files/{file\_id}”](#delete-apiorgsorg_idagentsagent_idchatschat_idfilesfile_id)
Delete Chat File
Delete a chat file from S3 (when not shared) and the database.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| file\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/files/{file_id}/download`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/chats/{chat\_id}/files/{file\_id}/download”](#get-apiorgsorg_idagentsagent_idchatschat_idfilesfile_iddownload)
Download Chat File
Download a chat file’s content.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| chat\_id | path | `string (uuid)` | yes | |
| file\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
# Chats
> REST API reference for chats.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/chats`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/chats`
[Section titled “GET /api/orgs/{org\_id}/chats”](#get-apiorgsorg_idchats)
List Work Chats
List work chats with pagination and outcome-based stats.
Stats: throughput = count(SUCCESS outcomes), efficiency = avg cost per SUCCESS outcome.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| agent\_id | query | `string (uuid)` | no | |
| org\_id | path | `string (uuid)` | yes | |
| page | query | `integer` | no | |
| per\_page | query | `integer` | no | |
| resolution | query | `string` | no | Filter by how the work came out: success, partial, failed, stopped. This is the filter a needs-a-human queue wants — `status=active` returns successes, partials and idle chats alike. |
| since | query | `string` | no | Time period for stats: 24h, 7d, 30d |
| sort\_by | query | `string` | no | Sort field: created\_at, completed\_at, total\_cost\_usd, title |
| sort\_order | query | `string` | no | asc or desc |
| status | query | `string` | no | |
| tags | query | `string[]` | no | Filter by tags (ALL must match) |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `ChatWorkListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/chats`
[Section titled “POST /api/orgs/{org\_id}/chats”](#post-apiorgsorg_idchats)
Create Work Chat
Create a new work chat (manual trigger).
Validates the agent, checks for prompt injection, determines initial ChatStatus based on execution mode, and emits `chat.work_created` to Hatchet when the chat is ACTIVE.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | --------------- | -------- | ----------- |
| agent\_id | `string (uuid)` | yes | |
| description | `string` | no | |
| tags | `string[]` | no | |
| title | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `ChatWorkResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}”](#get-apiorgsorg_idchatschat_id)
Get Work Chat
Get a single work chat detail.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatWorkResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/chats/{chat_id}`
[Section titled “DELETE /api/orgs/{org\_id}/chats/{chat\_id}”](#delete-apiorgsorg_idchatschat_id)
Delete Work Chat
Soft-delete a work chat.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/outcomes`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/outcomes”](#get-apiorgsorg_idchatschat_idoutcomes)
List Outcomes
List all outcomes recorded for a chat.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OutcomeListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/chats/{chat_id}/outcomes`
[Section titled “POST /api/orgs/{org\_id}/chats/{chat\_id}/outcomes”](#post-apiorgsorg_idchatschat_idoutcomes)
Create Outcome
Record an outcome against a chat (manual / override).
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------- | -------- | -------- | ----------- |
| evidence | `string` | no | |
| goal | `string` | yes | |
| status | `string` | no | |
| summary | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `OutcomeResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/chats/{chat_id}/tags`
[Section titled “PATCH /api/orgs/{org\_id}/chats/{chat\_id}/tags”](#patch-apiorgsorg_idchatschat_idtags)
Update Chat Tags
Update tags on a work chat.
Tag names are also registered in the org’s shared tag registry (idempotent) so they appear in `GET /tags` and can be renamed/recolored there — see `ChatService.rename_tag`/`remove_tag` for how a registry change syncs back into a chat’s tag list.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | ---------- | -------- | ----------- |
| tags | `string[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `string[]` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/trace`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/trace”](#get-apiorgsorg_idchatschat_idtrace)
Get Chat Trace
Lineage tree for a chat, with the building blocks each chat ran.
The tree is rooted at the topmost ancestor and expanded downward, so the response holds ancestors, siblings and descendants — the card highlights `is_current` rather than asking for a second request per branch.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatTraceResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/browse`
[Section titled “GET /api/orgs/{org\_id}/chats/browse”](#get-apiorgsorg_idchatsbrowse)
Browse Org Chats
The org-wide All chats table: every agent’s chats in one list.
The same rows, filters, sort and page shape as the per-agent table (`/api/orgs/{org_id}/agents/{agent_id}/chats/browse`), read at org scope — both go through :func:`_chat_list_select` and :func:`chat_browse_conditions`, so a row cannot say one thing here and another there. What differs is the axis each one offers instead of the other’s: this one filters by `agent` (repeatable), the per-agent one by `revision` — a revision number only means something inside one agent, so mixing agents makes “v10” a filter that quietly matches unrelated work.
For the same reason `revision_version` on these rows is only ever a *pinned* version: resolving an agent’s active revision per row would be one query per agent on the page to fill a column that cannot be compared across them anyway. The org-wide table doesn’t render it.
Visibility is the per-agent table’s, unchanged: private (neutral) chats reach only their creator, and everything is scoped to one org after a membership check.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ----- | ---------------------------------------- | -------- | ---------------------------------------------------------------------- |
| agent | query | `string (uuid)[]` | no | Repeatable agent id filter. |
| archived | query | `boolean` | no | True returns archived chats only; false (default) returns active ones. |
| from | query | `string (date-time)` | no | Only chats whose last activity is at or after this. |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| order | query | `"asc"` \| `"desc"` | no | |
| org\_id | path | `string (uuid)` | yes | |
| q | query | `string` | no | Case-insensitive substring match on the chat title. |
| resolution | query | `string[]` | no | Repeatable ChatResolution filter. |
| sort | query | `"activity"` \| `"cost"` \| `"messages"` | no | |
| source | query | `string[]` | no | Repeatable ChatSource filter. |
| status | query | `string[]` | no | Repeatable ChatStatus filter. |
| to | query | `string (date-time)` | no | Only chats whose last activity is at or before this. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChatBrowseResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/statuses`
[Section titled “GET /api/orgs/{org\_id}/chats/statuses”](#get-apiorgsorg_idchatsstatuses)
List Chat Statuses
Batch status lookup for chats referenced from another surface.
Exists so a view holding N chat ids (a chat transcript’s `start_chat` tool rows, say) draws N status icons from ONE request instead of N detail fetches — and draws them from the same rollup the conversation sidebar uses, so one chat never shows two different states in one page.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | ----------------- | -------- | ---------------------------- |
| ids | query | `string (uuid)[]` | no | Chat ids to look up (max 50) |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `ChatStatusListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Connections
> REST API reference for connections.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/connections`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/connections`
[Section titled “GET /api/orgs/{org\_id}/connections”](#get-apiorgsorg_idconnections)
List Connections
List connections for the organisation, visible to the caller.
PRIVATE connections owned by someone else are omitted entirely (not just redacted). `wildcard_agent_count` is the number of org agents with a `"*"` tool grant — excluded from every connection’s `used_by_agent_ids`.
Omit `limit` to get every connection; `total` is a real `COUNT` of the visible set either way — never the size of the page.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__connections__ConnectionListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/connections/{connection_id}`
[Section titled “GET /api/orgs/{org\_id}/connections/{connection\_id}”](#get-apiorgsorg_idconnectionsconnection_id)
Get Connection
Get a single connection by its prefixed ID.
A PRIVATE connection owned by someone else 404s — indistinguishable from a connection that doesn’t exist.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ---- | --------------- | -------- | ----------- |
| connection\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__connections__ConnectionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/connections/{connection_id}`
[Section titled “PATCH /api/orgs/{org\_id}/connections/{connection\_id}”](#patch-apiorgsorg_idconnectionsconnection_id)
Update Connection
Update a connection’s status, display label and/or decision policy.
Any org member may manage connections for now — see issue #260. A PRIVATE connection owned by someone else 404s.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ---- | --------------- | -------- | ----------- |
| connection\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------------- | -------- | -------- | ----------- |
| decision\_mode | `string` | no | |
| label | `string` | no | |
| status | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__connections__ConnectionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/connections/{connection_id}`
[Section titled “DELETE /api/orgs/{org\_id}/connections/{connection\_id}”](#delete-apiorgsorg_idconnectionsconnection_id)
Delete Connection
Disconnect / remove a connection.
Any org member may remove a connection visible to them for now — see issue #260. A PRIVATE connection owned by someone else 404s.
Deleting a connection that agents depend on returns 409 with the dependent agent ids; re-issue with `?acknowledge_dependents=true` to proceed — the same shape `PATCH /{connection_id}/scope` uses for privatizing. Deletion is never refused outright, only never silent: removing the row takes the connection away from *every* agent, including the owner’s own.
**Parameters**
| Name | In | Type | Required | Description |
| ----------------------- | ----- | --------------- | -------- | ----------- |
| acknowledge\_dependents | query | `boolean` | no | |
| connection\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/connections/{connection_id}/owner`
[Section titled “PATCH /api/orgs/{org\_id}/connections/{connection\_id}/owner”](#patch-apiorgsorg_idconnectionsconnection_idowner)
Assign Connection Owner
Hand a connection to a different member. Admin/owner or current owner.
Ownership decides who a private connection is visible to and which agents reach it (`run_as=owner`), so this moves real capability — but it does not re-authenticate. The stored credential still belongs to whoever authorized it; moving the underlying account to a different person means reconnecting.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ---- | --------------- | -------- | ----------- |
| connection\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------------- | --------------- | -------- | ----------- |
| owner\_user\_id | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__connections__ConnectionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/connections/{connection_id}/scope`
[Section titled “PATCH /api/orgs/{org\_id}/connections/{connection\_id}/scope”](#patch-apiorgsorg_idconnectionsconnection_idscope)
Update Connection Scope
Convert a connection’s visibility scope (private ⇄ org).
A private row is only visible to its owner, so only the owner can re-scope it. A shared (org) row is visible to everyone, but visibility is not permission: re-scoping one requires an org admin/owner or the connection’s own owner — taking a shared credential away from the whole org is not a change any member should be able to make.
Privatizing a connection that agents depend on returns 409 with the dependent agent ids; re-issue with `acknowledge_dependents: true` to proceed. Making an ownerless row private records the actor as its owner (a private row with no owner would be visible to nobody).
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ---- | --------------- | -------- | ----------- |
| connection\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | --------- | -------- | ----------- |
| acknowledge\_dependents | `boolean` | no | |
| scope | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__connections__ConnectionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/connections/by-slug/{slug}`
[Section titled “GET /api/orgs/{org\_id}/connections/by-slug/{slug}”](#get-apiorgsorg_idconnectionsby-slugslug)
Get Connection By Slug
Get a single connection by its URL slug.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| slug | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__connections__ConnectionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/connections/catalog`
[Section titled “GET /api/orgs/{org\_id}/connections/catalog”](#get-apiorgsorg_idconnectionscatalog)
Get Catalog
Return the app-level connections catalog: one card per product.
Merges the static apps (:func:`iter_static_apps`) with dynamic Composio apps (every ENABLED auth-config toolkit not already covered by a static app’s Composio method, e.g. Freshdesk) and a synthetic “Custom MCP” entry. `connected` reflects whether the org has a connection (visible to the caller) whose `provider_key` matches the app.
`app_key` is unique across the returned items: a toolkit whose slug equals a static app’s key is folded into that app as an extra connect method (:func:`with_composio_method`), never emitted as a second card.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CatalogResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/connections/catalog/{app_key}/methods`
[Section titled “GET /api/orgs/{org\_id}/connections/catalog/{app\_key}/methods”](#get-apiorgsorg_idconnectionscatalogapp_keymethods)
Get Catalog App Methods
Return the ranked connect methods for one catalog app.
`app_key` is either a static app key (:data:`APP_MAP`), the synthetic `"mcp_server"` key, or an ENABLED Composio toolkit slug not covered by a static app.
A static app whose key is also an ENABLED Composio toolkit gets that managed method appended (mirroring the catalog’s fold — see :func:`with_composio_method`). Without it this branch returned only the native params form and the managed route was unreachable, even though the catalog advertised it.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| app\_key | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `MethodListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/connections/stewardship`
[Section titled “GET /api/orgs/{org\_id}/connections/stewardship”](#get-apiorgsorg_idconnectionsstewardship)
List Connections For Stewardship
Every connection in the org, including private ones. Admin/owner only.
Exists so an outage is diagnosable and an offboarding is answerable: a connection owned by someone who has left breaks the agents that depend on it, and with per-user visibility alone nobody else can see the row to work out why.
**Seeing is not using.** No credentials are returned, and the tool gateway never consults this route — an admin can see a member’s private connection here and still cannot point an agent at it. Ordinary listing (`GET /connections`) is unchanged and stays per-viewer.
Declared before `/{connection_id}` so the literal path wins over the parameterised one.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `StewardshipListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Conversation Resources
> REST API reference for conversation resources.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/chats/{chat_id}/artifacts`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/chats/{chat_id}/artifacts`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/artifacts”](#get-apiorgsorg_idchatschat_idartifacts)
List Conversation Artifacts
List all artifacts produced within a conversation (conversation-wide).
Phase 2 (chat-outcomes model): `Artifact.chat_id` is the sole NOT-NULL anchor; the old `assignment_id` subquery path is gone.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------- |
| 200 | Successful Response | `ConversationArtifactListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/files`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/files”](#get-apiorgsorg_idchatschat_idfiles)
List Conversation Files
List all files attached to a conversation (conversation-wide).
Returns every `File` row where `chat_id` matches the conversation. Because `chat_id` is the required NOT-NULL owner (Phase 2 chat-outcomes model), this is the authoritative read for all files in a conversation.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `ConversationFileListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/files/{file_id}/download`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/files/{file\_id}/download”](#get-apiorgsorg_idchatschat_idfilesfile_iddownload)
Download Conversation File
Download a file that belongs to this conversation.
The owning column is `files.chat_id` — membership in the conversation is sufficient to download any file attached to it.
Validates that the file belongs to both this conversation and this organisation before streaming the bytes from S3.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| file\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/related-memories`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/related-memories”](#get-apiorgsorg_idchatschat_idrelated-memories)
Get Conversation Related Memories
Return memories related to a conversation.
Delegates to `get_chat_related_memories` in agent\_memories.py. The legacy `/api/orgs/{org_id}/assignments/{chat_id}/related-memories` path has been removed; this is now the only endpoint for this resource.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/todos`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/todos”](#get-apiorgsorg_idchatschat_idtodos)
List Conversation Todos
List all todo items for a conversation, ordered by position then created\_at.
Mirrors the list the agent manages via the `write_todos` internal agent tool. All todo items are anchored on `chat_id` (`assignment_id` was dropped in Phase 2).
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `ConversationTodoListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/transcripts`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/transcripts”](#get-apiorgsorg_idchatschat_idtranscripts)
Get Conversation Transcripts
Lightweight index of per-step LLM transcripts for an entire conversation.
Returns all `agent_transcripts` rows ordered by `created_at` ascending then `step` ascending. Only scalar columns are selected — the heavy `request_messages`, `response_message`, `tool_definitions`, and `model_params` JSONB columns are **never loaded**. Instead, `request_message_count` gives the number of messages in `request_messages` (computed in Postgres via `jsonb_array_length`).
Use `GET /transcripts/steps/{step_id}` to lazy-load the full payload for any individual step.
Gated to ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------------- |
| 200 | Successful Response | `ConversationTranscriptIndexResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/transcripts/steps/{step_id}`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/transcripts/steps/{step\_id}”](#get-apiorgsorg_idchatschat_idtranscriptsstepsstep_id)
Get Transcript Step Detail
Full detail for a single transcript step — heavy JSONB included.
Returns `request_messages`, `response_message`, `tool_definitions`, and `model_params` for a single `agent_transcripts` row. Designed to be called lazily (on scroll / on selection) from the transcript debug page so the index renders instantly and the heavy payload is fetched only for the step being inspected.
The step must belong to the specified `chat_id` (i.e. `agent_transcripts.chat_id == chat_id`). Returns 404 if the step is not found or does not belong to this conversation.
Gated to ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| step\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------------ |
| 200 | Successful Response | `ConversationTranscriptStepResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Credits
> REST API reference for credits.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/credits`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/credits`
[Section titled “GET /api/orgs/{org\_id}/credits”](#get-apiorgsorg_idcredits)
Get Credits
Balance, floor, period, plan and resolved entitlements for this org.
**Read `enforced` before anything else.** It is the only field that says whether a balance means anything here, and it is false in three quite different situations that must all render as “credits do not gate this org” rather than as a zero: an enterprise plan (the counter runs and never stops anything), an org with no subscription row, and credits switched off platform-wide. In all of them `balance` is null.
Never raises for an unprovisioned org. `UNSUBSCRIBED` resolves to `plan = null` with the deny-everything entitlement set and a 200 — the permissive direction every earlier PR takes with that sentinel, because a customer looking at a broken billing state is better served by an empty panel than by a 500 or a frozen account.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CreditsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/credits/breakdown`
[Section titled “GET /api/orgs/{org\_id}/credits/breakdown”](#get-apiorgsorg_idcreditsbreakdown)
Get Credit Breakdown
Credits burned in a window, grouped by metric, by agent and by chat.
Burns only. Grants, expiries and adjustments are excluded because the question is “what consumed my credits”, and folding a grant into that makes the totals meaningless. Credits are reported **positive** for the same reason: the ledger stores a burn as a negative delta, but a consumption report reads as “this chat used 4,120 credits”, not “-4,120”.
The agent and chat dimensions are not on `credit_ledger` — a burn is keyed to a `usage_events` row, and that is what carries `chat_id`. So both groupings join through the event and then to `agent_chats`; work that belongs to no chat (a title generation, a scheduled sweep) buckets under `"(none)"` rather than vanishing, because credits it burned are still credits the customer paid.
Each grouping is capped at the top {limit} rows by spend. `truncated` says when that bit, so a panel can offer the full statement instead of implying these are all the consumers there were.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | -------------------------------- |
| days | query | `integer` | no | Window size in days, ending now. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `BreakdownResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/credits/ledger`
[Section titled “GET /api/orgs/{org\_id}/credits/ledger”](#get-apiorgsorg_idcreditsledger)
List Credit Ledger
The org’s credit statement, newest first, in the shared page envelope.
Every row carries a **projected** receipt, never the stored `meta`: the burn receipt embeds our provider cost and the per-model multiplier verbatim, and neither is a fact about the customer’s work. What survives is what answers their question — which model ran, how the charge was computed, and how much was metered.
`kind` is validated against the known set so a typo is a 422 rather than a silently empty statement, which is indistinguishable from “you have never spent anything”. `metric` is deliberately *not* validated the same way: the metric vocabulary grows without a migration, and rejecting an unknown one would break a client the day a new metric ships.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | -------------------------------------------------------------------------------------------------- |
| kind | query | `string` | no | Filter to one ledger kind: grant \| burn \| adjustment \| purchase \| expiry. Omit for every kind. |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| metric | query | `string` | no | Filter to one usage metric (burn rows only carry one). Omit for every metric. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_LedgerEntry_` |
| 422 | Validation Error | `HTTPValidationError` |
# Custom Tools
> REST API reference for custom tools.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/custom-tools`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/custom-tools`
[Section titled “GET /api/orgs/{org\_id}/custom-tools”](#get-apiorgsorg_idcustom-tools)
List Custom Tools
List custom tools for the organization.
Omit `limit` to get every tool; `total` is the full count either way.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ----- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| include\_usage | query | `boolean` | no | Populate `calls_30d`, `agent_count`, `reference_count` and `deletion_safety`. Left off they are null (= not computed, not zero). Opt-in because it adds a fixed five queries per call, two of which read the org’s live process definitions and agent instructions — worth it for the code-tools table, wasted on the resource pickers that also call this endpoint. |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | query | `string` | no | Filter by project id, or ‘none’ for uncategorized tools. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `Page_CustomToolResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/custom-tools`
[Section titled “POST /api/orgs/{org\_id}/custom-tools”](#post-apiorgsorg_idcustom-tools)
Create Custom Tool
Create a new custom tool. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------ | ---------- | -------- | ----------- |
| env\_vars | `string[]` | no | |
| name | `string` | yes | |
| source\_code | `string` | yes | |
| tags | `string[]` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `CustomToolResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/custom-tools/{id_or_slug}`
[Section titled “GET /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}”](#get-apiorgsorg_idcustom-toolsid_or_slug)
Get Custom Tool
Get a custom tool by ID or slug.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CustomToolResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/custom-tools/{id_or_slug}`
[Section titled “PUT /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}”](#put-apiorgsorg_idcustom-toolsid_or_slug)
Update Custom Tool
Update a custom tool. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------ | ---------- | -------- | ----------- |
| env\_vars | `string[]` | no | |
| is\_enabled | `boolean` | no | |
| name | `string` | no | |
| source\_code | `string` | no | |
| tags | `string[]` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CustomToolResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/custom-tools/{id_or_slug}`
[Section titled “DELETE /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}”](#delete-apiorgsorg_idcustom-toolsid_or_slug)
Delete Custom Tool
Delete a custom tool. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/custom-tools/{id_or_slug}/execute`
[Section titled “POST /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}/execute”](#post-apiorgsorg_idcustom-toolsid_or_slugexecute)
Execute Custom Tool
Execute a custom tool (by ID or slug) with provided arg bindings.
Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------- | -------- | -------- | ----------- |
| arg\_bindings | `object` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `CustomToolRunResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/custom-tools/{id_or_slug}/revisions`
[Section titled “GET /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}/revisions”](#get-apiorgsorg_idcustom-toolsid_or_slugrevisions)
List Custom Tool Revisions
List a code tool’s version history, newest first (by tool ID or slug).
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `CustomToolRevisionSummary[]` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/custom-tools/{id_or_slug}/runs`
[Section titled “GET /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}/runs”](#get-apiorgsorg_idcustom-toolsid_or_slugruns)
List Tool Runs
List recent runs for a custom tool.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ----- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `CustomToolRunResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/custom-tools/{id_or_slug}/runs/{run_id}`
[Section titled “GET /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}/runs/{run\_id}”](#get-apiorgsorg_idcustom-toolsid_or_slugrunsrun_id)
Get Tool Run
Get a specific tool run by ID.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `CustomToolRunResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/custom-tools/{id_or_slug}/usage`
[Section titled “GET /api/orgs/{org\_id}/custom-tools/{id\_or\_slug}/usage”](#get-apiorgsorg_idcustom-toolsid_or_slugusage)
Get Custom Tool Usage
Who still uses this code tool — calls, process references, instruction references.
A dedicated sub-resource rather than fields on `GET /{id_or_slug}`, for the same reason `/runs` is one: the tool row is fetched on every render of the tool page and by the editor, while this needs a per-agent aggregate plus a scan of every live process definition and agent instruction in the org. Folding it in would put that cost on paths that never show it.
Requires org membership — reading usage is a member right, like history.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ----- | --------------- | -------- | ------------------------------------------------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| window\_days | query | `integer` | no | Call window in days. References are not windowed. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `CustomToolUsageResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/custom-tools/validate`
[Section titled “POST /api/orgs/{org\_id}/custom-tools/validate”](#post-apiorgsorg_idcustom-toolsvalidate)
Validate Source
Validate Python source code without saving. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------ | ---------- | -------- | ----------- |
| env\_vars | `string[]` | no | |
| source\_code | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `ValidateSourceResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Decisions
> REST API reference for decisions.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agents/{agent_id}/decisions`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agents/{agent_id}/decisions`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/decisions”](#get-apiorgsorg_idagentsagent_iddecisions)
List Agent Decisions
Every decision this agent recorded, across all its chats, newest first.
`agent_id` accepts the id or the slug, like the other agent routes.
The per-agent view of the same rows the chat’s Decisions tab shows. Newest first because an agent’s history is a feed — what has it been deciding lately — where a single chat is read forwards as a sequence.
Only decisions from chats the caller can open are listed (a private chat’s decisions quote its content). Served whatever the decision log setting says: switching the feature off never hides decisions already recorded.
Filters: `review_status` (`unreviewed` | `agreed` | `disputed` | `reverted`), `kind` (`act` | `no_act` | `escalate`), and the two flags `deviation` / `determination`. Unknown values are refused rather than silently matching nothing. The flag pair is FILTER-ONLY and one-directional: off (the default) applies no filter at all, on narrows to rows carrying that flag. There is deliberately no “exclude flagged rows” mode — a deviation is the one a reviewer must not miss, so the UI promotes it rather than making you filter toward it.
`archived` (default `false`) selects which of two disjoint pages this is: the live queue (excludes archived rows) or the Archived tab (archived rows only). There is no combined view — see `POST .../decisions/archive`.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| agent\_id | path | `string` | yes | |
| archived | query | `boolean` | no | |
| determination | query | `boolean` | no | |
| deviation | query | `boolean` | no | |
| kind | query | `string` | no | |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| review\_status | query | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AgentDecisionPage` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/decisions/archive`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/decisions/archive”](#post-apiorgsorg_idagentsagent_iddecisionsarchive)
Archive Decisions
Archive one or many decisions WITHOUT recording a verdict.
Clears them from the Unreviewed queue for the case a verdict cannot honestly describe — hundreds of routine, unremarkable decisions nobody is going to individually agree or dispute. Orthogonal to review: an archived decision keeps whatever `review_status` it already had.
Body is either `{"decision_ids": [...]}` (at most `MAX_BULK_DECISION_IDS`) or `{"filter": {...}}` — exactly one, else 422. A filter with every field omitted archives every currently non-archived decision the caller can see (e.g. “archive everything”); `{"filter": {"review_status": "unreviewed"}}` archives only the unreviewed backlog.
Same authz and scoping as the rest of this page: any org member, and only rows reachable through a chat the caller could open (private chats excepted). Archiving an already-archived row is a no-op for it — a repeat “archive all unreviewed” sweep does not reset `archived_at`.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------- | -------------------- | -------- | ----------- |
| decision\_ids | `string (uuid)[]` | no | |
| filter | `DecisionBulkFilter` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `DecisionBulkUpdateResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/agents/{agent_id}/decisions/groups`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/decisions/groups”](#get-apiorgsorg_idagentsagent_iddecisionsgroups)
List Agent Decision Groups
This agent’s decisions grouped by the exact `basis` they cite, largest group first — “what rule keeps showing up” — so a reviewer can archive or agree with a whole group instead of triaging it row by row.
Same visibility, retention and filter vocabulary as `GET /{agent_id}/decisions` (`review_status`, `kind`, the `deviation` / `determination` flags, `archived` — all optional, `archived` defaulting to `false` to match that endpoint’s default view). Paginated the same way; `total` counts matching GROUPS, not decisions.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| agent\_id | path | `string` | yes | |
| archived | query | `boolean` | no | |
| determination | query | `boolean` | no | |
| deviation | query | `boolean` | no | |
| kind | query | `string` | no | |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| review\_status | query | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `Page_DecisionGroupItem_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/decisions/review`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/decisions/review”](#post-apiorgsorg_idagentsagent_iddecisionsreview)
Bulk Review Decisions
Bulk-apply the `agreed` verdict — the review-side twin of `archive`, for the decisions a reviewer wants to affirmatively wave through in bulk rather than one at a time.
Same body shape as `archive`, plus a top-level `review_status` that must be `"agreed"`. Only currently UNREVIEWED, non-archived rows are ever touched — an existing verdict from a prior single or bulk review is never overwritten, so `filter.review_status`, if given, must be `"unreviewed"` (or omitted; both mean the same thing here).
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------------- | -------------------- | -------- | ----------- |
| decision\_ids | `string (uuid)[]` | no | |
| filter | `DecisionBulkFilter` | no | |
| review\_status | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `DecisionBulkUpdateResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/decisions/unarchive`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/decisions/unarchive”](#post-apiorgsorg_idagentsagent_iddecisionsunarchive)
Unarchive Decisions
The inverse of `archive` — brings decisions back onto the pages they would otherwise appear on (the Unreviewed queue, or wherever their `review_status` already put them). Same body shape, same scoping.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------- | -------------------- | -------- | ----------- |
| decision\_ids | `string (uuid)[]` | no | |
| filter | `DecisionBulkFilter` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `DecisionBulkUpdateResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/chats/{chat_id}/decisions`
[Section titled “GET /api/orgs/{org\_id}/chats/{chat\_id}/decisions”](#get-apiorgsorg_idchatschat_iddecisions)
List Chat Decisions
Every decision recorded in this chat, oldest first.
Oldest first because a reviewer reads a run forwards: the order the decisions were taken in is the order they make sense in.
Served whatever the org switch says — see the module docstring.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ----- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| limit | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `DecisionListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/chats/{chat_id}/decisions/{decision_id}/review`
[Section titled “POST /api/orgs/{org\_id}/chats/{chat\_id}/decisions/{decision\_id}/review”](#post-apiorgsorg_idchatschat_iddecisionsdecision_idreview)
Review Decision
Record a human’s verdict on one decision.
`agreed` marks the row and nothing else happens in v1 — there is no fingerprint learning yet, and pretending otherwise would be the more expensive lie.
`disputed` stores `should_have`, which is what an instruction-revision draft is written from. The draft itself is a separate step so a reviewer can disagree without immediately being asked to author a rule.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| decision\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------------- | -------- | -------- | ----------- |
| review\_note | `string` | no | |
| review\_status | `string` | yes | |
| should\_have | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `DecisionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/chats/{chat_id}/decisions/{decision_id}/revision-draft`
[Section titled “POST /api/orgs/{org\_id}/chats/{chat\_id}/decisions/{decision\_id}/revision-draft”](#post-apiorgsorg_idchatschat_iddecisionsdecision_idrevision-draft)
Draft Revision From Decision
Append this disagreement to the agent’s instruction DRAFT.
This is the far end of the review loop, and the reason `basis` exists: a disagreement lands on a known sentence instead of a vibe.
**A draft, never a deploy.** Instruction changes are proposals a human publishes — the same rule `update_own_instruction` follows — so this writes into the existing draft (creating one from the active revision if there is none) and leaves publishing to the agent’s own screen.
Requires the decision to be `disputed` with a `should_have`: without the alternative there is nothing to write, which is why the review endpoint insists on it.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| chat\_id | path | `string (uuid)` | yes | |
| decision\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `RevisionDraftResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Effort
> REST API reference for effort.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/model-modes`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/model-modes`
[Section titled “GET /api/orgs/{org\_id}/model-modes”](#get-apiorgsorg_idmodel-modes)
List Model Modes
The effort levels this org may pick, with each level’s credit burn ratio.
Member-level: choosing a level is an ordinary chat-composer action, and the agent form is already gated separately on admin/owner.
`direct_model_choice` tells the caller which of the two selection UIs applies. It comes from :class:`EntitlementService`, so an org with no subscription resolves to `False` — effort levels — which is the state that still yields a working run. Note that this is the *only* entitlement in play: the level list itself is the platform’s, identical for every plan.
The ratios are derived on every call from live catalog prices, so they cannot drift from what the ledger will actually charge. A level with no configured preset, or one whose models the catalog cannot price, reports `burn_ratio: null`.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `EffortLevelsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Envd
> REST API reference for envd.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/envd/agentdepot-envd.pyz`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/envd/agentdepot-envd.pyz`
[Section titled “GET /envd/agentdepot-envd.pyz”](#get-envdagentdepot-envdpyz)
Daemon Zipapp
The daemon itself, as one self-contained file.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---- |
| 200 | Successful Response | |
### GET `/install.sh`
[Section titled “GET /install.sh”](#get-installsh)
Install Script
The `curl | sh` installer, pointed at whichever deployment served it.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---- |
| 200 | Successful Response | |
# Environments
> REST API reference for environments.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/environments`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/environments`
[Section titled “GET /api/orgs/{org\_id}/environments”](#get-apiorgsorg_idenvironments)
List Environments
List the org’s environments (built-in first — it is created first).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `EnvironmentResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/environments`
[Section titled “POST /api/orgs/{org\_id}/environments”](#post-apiorgsorg_idenvironments)
Create Environment
Create an environment.
A built-in sandbox’s name is not the caller’s to choose: there is at most one per org and it is always called “Built-in sandbox”, so `name` is ignored entirely for `BUILTIN_SANDBOX` (older clients may still send one). A remote environment, by contrast, requires a name — there can be many.
For a remote environment this also mints the daemon token and returns the install one-liner. That response is the only place the plaintext token ever appears — losing it means creating a new environment.
A custom-image environment needs an `image_ref`, is capped by the org’s `max_custom_environments` entitlement, and comes back `validating`: the pull and the conformance probe run on the agent-runner and the row settles on `online` or `invalid` when they finish. Poll `GET` for it, exactly as the create-modal already polls a remote machine waiting to call home.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | ----------------- | -------- | ----------- |
| image\_ref | `string` | no | |
| kind | `EnvironmentKind` | yes | |
| name | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 201 | Successful Response | `CreateEnvironmentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/environments/{environment_id}`
[Section titled “GET /api/orgs/{org\_id}/environments/{environment\_id}”](#get-apiorgsorg_idenvironmentsenvironment_id)
Get Environment
Fetch one environment. The create-modal polls this until `status` is `online` — i.e. until the machine has called home.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `EnvironmentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/environments/{environment_id}`
[Section titled “PATCH /api/orgs/{org\_id}/environments/{environment\_id}”](#patch-apiorgsorg_idenvironmentsenvironment_id)
Update Environment
Edit an environment’s operator-owned settings.
The one that matters here is `exec_timeout_s` — how long a single command may run. It is a **ceiling**, not a default: an agent’s `bash` call asks for the time it needs and is clamped to this, so raising it does not make every trivial command hold a long lease.
The value is clamped into the platform’s bounds rather than rejected — the bounds are ours, not the caller’s mistake — and the response reports the ceiling that will actually be enforced, which for an unset environment is its per-kind default rather than the `null` that is stored.
`image_ref` is the other one, and it is the *Update* button for a custom image: it re-points the environment, **drops the pinned digest** and sends it back through validation. That is the only way a new push to the same tag ever takes effect — a validated environment follows a digest, never a tag, so nothing changes under a running agent until somebody asks for it. Note that dropping the pin is not merely bookkeeping: it is the signal the validator reads to decide it should resolve the reference again rather than re-probe the bytes `POST /validate` would have re-probed.
`name` is the operator’s label for a machine or an image they set up. The built-in sandbox is not one of those: it is the same container pool for every org, named by us and described everywhere in the product by that name, so renaming it is refused rather than quietly ignored.
Admin-only: it decides how long an agent may hold a machine the org owns, and which image runs on our infrastructure.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| -------------------- | --------- | -------- | ----------- |
| exec\_timeout\_reset | `boolean` | no | |
| exec\_timeout\_s | `integer` | no | |
| image\_ref | `string` | no | |
| name | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `EnvironmentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/environments/{environment_id}`
[Section titled “DELETE /api/orgs/{org\_id}/environments/{environment\_id}”](#delete-apiorgsorg_idenvironmentsenvironment_id)
Delete Environment
Delete an environment.
Agents and chats pointing at it keep working: the FK is `SET NULL`, so they fall back to the resolution order rather than breaking.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/environments/{environment_id}/activity`
[Section titled “GET /api/orgs/{org\_id}/environments/{environment\_id}/activity”](#get-apiorgsorg_idenvironmentsenvironment_idactivity)
Get Environment Activity
What has actually run here — the audit `sandbox_runs` has always kept.
Every one of these rows was already being written on every acquire and teardown; nothing in the product read them back. That is why an org whose agents mysteriously stopped getting a workspace had no way to see that the last four runs failed, and why “is anybody using this environment?” was a question only answerable by deleting it.
A separate request from the environment itself, and deliberately not folded into the list: the settings page **polls** the list, and a reporting join over the run history has no business running every few seconds against every environment an org owns. This is fetched once, when somebody opens the panel.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ----- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `EnvironmentActivityResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/environments/{environment_id}/ping`
[Section titled “POST /api/orgs/{org\_id}/environments/{environment\_id}/ping”](#post-apiorgsorg_idenvironmentsenvironment_idping)
Ping Environment
Actually talk to the machine, and report what happened.
`ping` is the one verb every daemon has ever served, and the daemon deliberately does not count it as activity — it never touches the workspace and never renews a pinned workspace’s claim — so this is safe to press against a machine somebody is working on.
It answers a question no stored field can. `status` and `last_seen_at` are both history; `connected` says a socket exists. Only a round trip says the machine on the other end is still answering, which is what an operator who has just changed a firewall rule or woken a laptop is asking.
Failures come back as `ok: false` with a sentence, never as an error status: the request succeeded, the machine is what did not.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `EnvironmentPingResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/environments/{environment_id}/reveal-token`
[Section titled “POST /api/orgs/{org\_id}/environments/{environment\_id}/reveal-token”](#post-apiorgsorg_idenvironmentsenvironment_idreveal-token)
Reveal Environment Token
Read a remote machine’s *current* daemon token back, in the clear.
The View Install Command dialog’s other button: unlike `rotate-token`, this mints nothing and changes nothing on the row — it only decrypts the copy already stored and hands it back, so an admin who still has a working token installed somewhere can see it again without re-keying (and breaking) every other machine on this environment.
**Why this is safe to expose at all**: whoever may call it — an org admin, the same gate as `rotate-token` — could already mint a token that works with no confirmation beyond the click, so reading the existing one back grants no new capability; it is strictly the less disruptive of the two ways to end up holding a working token. See `EnvironmentService.reveal_token` for the full rationale.
`POST` rather than `GET` on purpose — nothing should prefetch or cache a secret. The response also carries `Cache-Control: no-store`, belt and braces against any intermediary that caches POST responses.
404 for no such environment, 422 for a kind with nothing to reveal (not a remote machine), 409 for a remote machine whose token predates this feature — the row has never had an encrypted copy stored, so there is nothing here to decrypt and rotating once is the only way to get one.
Admin-only, like every other endpoint that hands back a working credential.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `RevealTokenResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/environments/{environment_id}/rotate-token`
[Section titled “POST /api/orgs/{org\_id}/environments/{environment\_id}/rotate-token”](#post-apiorgsorg_idenvironmentsenvironment_idrotate-token)
Rotate Environment Token
Re-key a remote machine and hand back a fresh install command.
The fresh plaintext token is returned directly here, same response shape as a create — and, like a freshly created one, it can also be read back again later through `POST .../reveal-token` without another rotation.
Two things it is for: an install command that was lost, and a machine being replaced. Before it, both meant deleting the environment and creating another, which `SET NULL`s every agent and chat pointing at it and throws away the run history — a destructive operation standing in for a credential rotation.
**The currently connected daemon keeps serving.** Authentication happens at connect, so an established socket is unaffected and only the next connect needs the new token. Say so in the UI: the natural assumption is the opposite, and an operator who believes this cuts a machine off will never press it.
Admin-only — it mints a credential.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `CreateEnvironmentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/environments/{environment_id}/usage`
[Section titled “GET /api/orgs/{org\_id}/environments/{environment\_id}/usage”](#get-apiorgsorg_idenvironmentsenvironment_idusage)
Get Environment Usage
Which agents and chats point at this environment.
The delete dialog’s honest version. It has always promised that agents “fall back to their next configured environment” — true, and useless on its own, because the operator still cannot see *which* agents are about to start running somewhere else. Naming them is the difference between a warning and a decision.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `EnvironmentUsageResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/environments/{environment_id}/validate`
[Section titled “POST /api/orgs/{org\_id}/environments/{environment\_id}/validate”](#post-apiorgsorg_idenvironmentsenvironment_idvalidate)
Revalidate Environment
Re-run validation against the **pinned digest** — the *Re-validate* button.
Distinct from `PATCH` with an `image_ref`, and the difference is which bytes get probed. This re-checks the exact image the org’s agents are running and never re-resolves the tag, so pressing it cannot move an environment onto a push nobody has approved; *Update* is the one that re-resolves and re-pins, because it is deliberately pointing at something we have not looked at.
That split is what makes the product’s headline promise true — we pin the digest, so a new push to the same tag changes nothing until somebody asks for it — and neither this endpoint nor the event it pushes carries a mode to say so: a pinned digest on the row *is* the instruction, and the worker reads it there.
The other reason it exists, and the one it is now actually shaped for: our own sandbox contract moves. An image validated against contract 1 keeps working when contract 2 ships (our release never takes a customer’s agents offline), and re-probing the pinned bytes is exactly how its owner finds out what a rebuild would gain them — a question about *this* image, which a tag re-resolution would have quietly answered about a different one.
An environment that never pinned anything — it failed before the digest was resolved — has nothing to re-check, so this falls back to pulling the stored reference. That is the only thing it could mean, and it is still useful: it is how a rejected environment gets a fresh verdict.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| environment\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `EnvironmentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/environments/custom-images/registries`
[Section titled “GET /api/orgs/{org\_id}/environments/custom-images/registries”](#get-apiorgsorg_idenvironmentscustom-imagesregistries)
List Allowed Registries
The registry hosts we will pull a custom image from.
Served rather than hard-coded in the client for the usual reason: the allowlist is a security decision that changes on our side, and a create form listing hosts we no longer accept is a form that produces confident 422s.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `string[]` |
| 422 | Validation Error | `HTTPValidationError` |
# Files
> REST API reference for files.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/files`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/files`
[Section titled “GET /api/orgs/{org\_id}/files”](#get-apiorgsorg_idfiles)
List Files
List files in the organization, newest first.
Mirrors the detail endpoint’s scoping: org membership is sufficient, since `org_id` is a direct column on every file — this surfaces exactly what `GET /{file_id}` already lets a member read one at a time, it does not widen access.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `Page_FileDetailResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/files/{file_id}`
[Section titled “GET /api/orgs/{org\_id}/files/{file\_id}”](#get-apiorgsorg_idfilesfile_id)
Get File
Get a single file’s metadata by ID.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| file\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `FileDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/files/{file_id}/download`
[Section titled “GET /api/orgs/{org\_id}/files/{file\_id}/download”](#get-apiorgsorg_idfilesfile_iddownload)
Download File
Download a file (forces a native browser download).
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| file\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/files/{file_id}/view`
[Section titled “GET /api/orgs/{org\_id}/files/{file\_id}/view”](#get-apiorgsorg_idfilesfile_idview)
View File
Serve a file inline so it renders in a browser tab / iframe / img.
Identical to /download but uses `Content-Disposition: inline`.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| -------- | ---- | --------------- | -------- | ----------- |
| file\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
# Human-in-the-Loop
> REST API reference for human-in-the-loop.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/hitl-requests`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/hitl-requests`
[Section titled “GET /api/orgs/{org\_id}/hitl-requests”](#get-apiorgsorg_idhitl-requests)
List Hitl Requests
List an organization’s HITL requests, newest first.
Defaults to `pending` — the questions still waiting on a human. Pass `status` to read the history instead. The value is typed, so FastAPI rejects anything outside :data:`HitlStatus` with a 422 before the handler runs; a filter this endpoint cannot honour is never silently dropped.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | -------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| status | query | `"pending"` \| `"approved"` \| `"rejected"` \| `"responded"` \| `"cancelled"` \| `"expired"` | no | Return only requests with this status. Omit for the pending inbox (the default). An unrecognised value is rejected with 422 rather than quietly falling back to pending. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `HitlListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/hitl-requests/{request_id}`
[Section titled “GET /api/orgs/{org\_id}/hitl-requests/{request\_id}”](#get-apiorgsorg_idhitl-requestsrequest_id)
Get Hitl Request
Get a single HITL request.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| request\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `HitlRequestResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/hitl-requests/{request_id}/respond`
[Section titled “POST /api/orgs/{org\_id}/hitl-requests/{request\_id}/respond”](#post-apiorgsorg_idhitl-requestsrequest_idrespond)
Respond To Hitl Request
Respond to a pending HITL request and resume the chat.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| request\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | -------- | -------- | ----------- |
| action | `string` | no | |
| selection | `string` | no | |
| selections | `object` | no | |
| text | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `HitlRequestResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Inbox
> REST API reference for inbox.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `POST https://api.agentdepot.org/api/inbox/{agent_slug}/{code}`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### POST `/api/inbox/{agent_slug}/{code}`
[Section titled “POST /api/inbox/{agent\_slug}/{code}”](#post-apiinboxagent_slugcode)
Receive Webhook Short
Create a work chat via public webhook inbox (simplified URL without org slug).
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | -------- | -------- | ----------- |
| agent\_slug | path | `string` | yes | |
| code | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------------------------- |
| 201 | Successful Response | `agentdepot_api__routers__inbox__InboxResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/inbox/{org_slug}/{agent_slug}/{code}`
[Section titled “POST /api/inbox/{org\_slug}/{agent\_slug}/{code}”](#post-apiinboxorg_slugagent_slugcode)
Receive Webhook
Create a work chat via public webhook inbox. No authentication required.
Accepts two content types:
* `application/json`: JSON body with title, description, links, file\_urls
* `multipart/form-data`: form field `metadata` (JSON string) + file parts
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | -------- | -------- | ----------- |
| agent\_slug | path | `string` | yes | |
| code | path | `string` | yes | |
| org\_slug | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------------------------- |
| 201 | Successful Response | `agentdepot_api__routers__inbox__InboxResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/inbox/hitl/{token}`
[Section titled “POST /api/inbox/hitl/{token}”](#post-apiinboxhitltoken)
Receive Hitl Reply
Ingest a human’s email reply to a HITL request (no auth — token + secret).
Resolves the signed reply-address token back to the originating request. If it is still pending, the reply becomes the HITL response and the chat resumes; if it was already answered, the reply continues the conversation as a new message.
**Parameters**
| Name | In | Type | Required | Description |
| ----- | ---- | -------- | -------- | ----------- |
| token | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__inbox__InboxResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Inboxes
> REST API reference for inboxes.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/inboxes`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/inboxes`
[Section titled “GET /api/orgs/{org\_id}/inboxes”](#get-apiorgsorg_idinboxes)
List Inboxes
Inbox list screen: name, channels, 24h volume/verdict split, status.
The two `target_*` filters are what the agent page’s Inboxes card reads — an agent no longer owns an inbox, so “which addresses reach this agent” is a query over the inboxes pointed at it, not a field on the agent.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ----- | ------------------------ | -------- | ------------------------------------------------------------ |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | query | `string` | no | Filter by project id, or ‘none’ for uncategorized inboxes. |
| target\_id | query | `string (uuid)` | no | Only inboxes whose default handler is this agent or process. |
| target\_kind | query | `"agent"` \| `"process"` | no | Only inboxes whose default handler is of this kind. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_InboxResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes`
[Section titled “POST /api/orgs/{org\_id}/inboxes”](#post-apiorgsorg_idinboxes)
Create Inbox
Create an inbox. Requires admin/owner.
With no `target_*` and no `channels` this is the bare shell it always was — an inbox that receives nothing and routes nowhere until the Channels and Settings screens fill it in. Passing them stands the whole thing up in one transaction instead; no revision either way, which is an inbox that accepts everything and hands it to its default handler.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------ | ------------------------ | -------- | ----------- |
| channels | `"email" \| "webhook"[]` | no | |
| name | `string` | yes | |
| slug | `string` | yes | |
| target\_id | `string (uuid)` | no | |
| target\_kind | `"agent"` \| `"process"` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------------------------- |
| 201 | Successful Response | `agentdepot_api__routers__inboxes__InboxResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/inboxes/{inbox_id}`
[Section titled “GET /api/orgs/{org\_id}/inboxes/{inbox\_id}”](#get-apiorgsorg_idinboxesinbox_id)
Get Inbox
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__inboxes__InboxResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/inboxes/{inbox_id}`
[Section titled “PATCH /api/orgs/{org\_id}/inboxes/{inbox\_id}”](#patch-apiorgsorg_idinboxesinbox_id)
Update Inbox
Settings screen: retention, rate limits, default handler, name/status.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------------------------- | --------------- | -------- | ----------- |
| name | `string` | no | |
| rate\_limit\_classifier\_calls\_per\_day | `integer` | no | |
| rate\_limit\_messages\_per\_day | `integer` | no | |
| rate\_limit\_per\_sender\_per\_hour | `integer` | no | |
| rate\_limit\_per\_sender\_per\_minute | `integer` | no | |
| retention\_days | `integer` | no | |
| settings | `object` | no | |
| spike\_protection\_enabled | `boolean` | no | |
| status | `string` | no | |
| target\_id | `string (uuid)` | no | |
| target\_kind | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__inboxes__InboxResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/inboxes/{inbox_id}`
[Section titled “DELETE /api/orgs/{org\_id}/inboxes/{inbox\_id}”](#delete-apiorgsorg_idinboxesinbox_id)
Delete Inbox
Soft-delete an inbox and disable its channels. Requires admin/owner.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/inboxes/{inbox_id}/channels`
[Section titled “GET /api/orgs/{org\_id}/inboxes/{inbox\_id}/channels”](#get-apiorgsorg_idinboxesinbox_idchannels)
List Channels
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `InboxChannelResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/channels`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/channels”](#post-apiorgsorg_idinboxesinbox_idchannels)
Create Channel
Create a channel. An inbox may hold more than one of each kind.
Email: always gets a real 6-char `address_token`, whether or not it is rendered as part of the address — `has_code` only sets `config["public"]` (`False` = the code shows in the address; the default `True` is the codeless, public configuration). Webhook: gets an opaque `address_token` and nothing else — that token is the whole credential, so the response’s `webhook_url` is itself sensitive.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | --------- | -------- | --------------------- |
| config | `object` | no | |
| has\_code | `boolean` | no | |
| kind | `string` | yes | ’email’ or ‘webhook’. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 201 | Successful Response | `InboxChannelResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/inboxes/{inbox_id}/channels/{channel_id}`
[Section titled “PATCH /api/orgs/{org\_id}/inboxes/{inbox\_id}/channels/{channel\_id}”](#patch-apiorgsorg_idinboxesinbox_idchannelschannel_id)
Update Channel
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| channel\_id | path | `string (uuid)` | yes | |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | --------- | -------- | ----------- |
| config | `object` | no | |
| enabled | `boolean` | no | |
| has\_code | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `InboxChannelResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/inboxes/{inbox_id}/channels/{channel_id}`
[Section titled “DELETE /api/orgs/{org\_id}/inboxes/{inbox\_id}/channels/{channel\_id}”](#delete-apiorgsorg_idinboxesinbox_idchannelschannel_id)
Delete Channel
Hard-delete — `InboxChannel` carries no soft-delete column.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| channel\_id | path | `string (uuid)` | yes | |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/channels/{channel_id}/rotate-token`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/channels/{channel\_id}/rotate-token”](#post-apiorgsorg_idinboxesinbox_idchannelschannel_idrotate-token)
Rotate Channel Token
Mint a fresh address token for a webhook channel. Requires admin/owner.
This is revocation: the token in the URL is what authenticates an inbound webhook, so rotating it kills the old URL the instant this commits. Every sender configured with it starts getting 404s and has to be given the new one — which is the point, and why it is not something an update can do by accident. The response carries the new `webhook_url`; the old one is not recoverable.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| channel\_id | path | `string (uuid)` | yes | |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `InboxChannelResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/inboxes/{inbox_id}/messages`
[Section titled “GET /api/orgs/{org\_id}/inboxes/{inbox\_id}/messages”](#get-apiorgsorg_idinboxesinbox_idmessages)
List Messages
Inbox -> Messages screen: every message, whatever happened to it.
**Parameters**
| Name | In | Type | Required | Description |
| ----------------- | ----- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| channel | query | `string` | no | Filter by channel kind: email\|webhook. |
| evaluation\_state | query | `string` | no | Filter by pipeline state: evaluating\|settled. ‘evaluating’ is the in-progress view — messages that have arrived but whose rules are still running. |
| inbox\_id | path | `string` | yes | |
| lane | query | `string` | no | Filter to one of the screen’s lanes: needs\_attention (held, evaluating and failed work) \| all \| accepted \| dropped. |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| search | query | `string` | no | Substring match over sender/subject. |
| state | query | `string[]` | no | Filter by message state — repeatable, OR’d together: accepted\|failed\|held\|evaluating\|dropped. One value per row (they partition the log), which `verdict` alone cannot do: it reports an in-flight message as held and an accepted one whose agent crashed as accepted. Passing an empty list is a real filter and returns nothing. |
| verdict | query | `string` | no | Filter by verdict: accepted\|dropped\|quarantined. ‘quarantined’ returns settled messages only — a message still being evaluated carries that verdict provisionally and is not held for anyone. |
| within\_hours | query | `integer` | no | Only messages received in the last N hours. Omit for the whole log. A window rather than an absolute instant so the cutoff is computed once, here, from one clock — a client that sends its own timestamp sends a different one on every render. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------------- |
| 200 | Successful Response | `Page_InboxMessageIndexResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/inboxes/{inbox_id}/messages/{message_id}`
[Section titled “GET /api/orgs/{org\_id}/inboxes/{inbox\_id}/messages/{message\_id}”](#get-apiorgsorg_idinboxesinbox_idmessagesmessage_id)
Get Message
Message detail: envelope, the per-stage trace in stage order (rendered as-is — the evaluator already writes it in execution order), raw-expiry.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| message\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `InboxMessageDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/messages/{message_id}/replay`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/messages/{message\_id}/replay”](#post-apiorgsorg_idinboxesinbox_idmessagesmessage_idreplay)
Replay Message
Run *any* message through the pipeline again, whatever it was decided.
Retry only ever rescued mail nobody had decided. This is the other half: an accepted message whose chat went nowhere, or a dropped one a since-fixed rule should have let in. Both were previously dead ends — the only way to re-run either was to make the sender send it again.
Still re-evaluates rather than forcing: a replayed message that fails the gates is held or dropped again. A replay of accepted mail dispatches a *second* chat and leaves the first alone; the message row points at the new chat and keeps the old id in its trace.
Any member, not admin-only like retry: a replay spends the same classifier call a retry does, and gating the fix behind a role while leaving the inbound flood ungated protects nothing.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| message\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `InboxMessageDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/messages/{message_id}/retry`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/messages/{message\_id}/retry”](#post-apiorgsorg_idinboxesinbox_idmessagesmessage_idretry)
Retry Message
Run a held message again against the rule set that is live now.
The recovery path quarantine never had. Every uncertain outcome in this pipeline holds a message — a classifier timeout, a provider 5xx, an unroutable model — and all of those are fixable, but fixing one never rescued the mail it had already stopped.
Re-evaluates; it does not force. A message that still fails the gates stays held, because the gates are the point. Admin or owner, because it can create a chat and spend a classifier call.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| message\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `InboxMessageDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/messages/{message_id}/terminate`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/messages/{message\_id}/terminate”](#post-apiorgsorg_idinboxesinbox_idmessagesmessage_idterminate)
Terminate Message
Stop whatever this message is still doing, and let it be restarted.
The lever the log was missing. Retry and replay both *start* something, and neither answers a message that is stuck: a replay of accepted mail leaves the original chat running on purpose, so a hung import replied to a replay by spending on two chats instead of none, and a row the pipeline died mid-evaluation on could not be touched at all for ten minutes.
Terminating stops and closes the dispatched chat and its whole subtree (which is where the spend usually is), releasing any question it left a human holding, cancels a dispatched process run, stands down an armed reply so the sender is not answered about work that was abandoned, and settles an `evaluating` row.
It does **not** re-decide the message: the verdict is left as the rules wrote it, and nothing is dispatched. Restarting afterwards is a replay, which now goes through immediately because the row is settled — subject to the usual limit that a payload past its retention window cannot be re-run at all.
Any member, like replay: this only ever stops work and never spends.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| message\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `InboxMessageDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/inboxes/{inbox_id}/messages/counts`
[Section titled “GET /api/orgs/{org\_id}/inboxes/{inbox\_id}/messages/counts”](#get-apiorgsorg_idinboxesinbox_idmessagescounts)
Message Counts
Counts for the messages screen’s lanes and filter chips.
Declared above `/{message_id}` on purpose: that route’s path parameter is a UUID, so a request for this one would otherwise be matched by it and rejected as a malformed id.
Takes the same `channel`/`search`/`within_hours` the list takes, and deliberately not `state`/`lane`: those are what the counts are *for*. Narrowing them by the chip the operator already clicked would leave every other chip reading zero.
**Parameters**
| Name | In | Type | Required | Description |
| ------------- | ----- | --------------- | -------- | ------------------------------------------- |
| channel | query | `string` | no | Filter by channel kind: email\|webhook. |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| search | query | `string` | no | Substring match over sender/subject/rule. |
| within\_hours | query | `integer` | no | Only messages received in the last N hours. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `InboxMessageCounts` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/inboxes/{inbox_id}/revisions`
[Section titled “GET /api/orgs/{org\_id}/inboxes/{inbox\_id}/revisions”](#get-apiorgsorg_idinboxesinbox_idrevisions)
List Revisions
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `InboxRevisionResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/revisions`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/revisions”](#post-apiorgsorg_idinboxesinbox_idrevisions)
Push Revision
Save the inbox’s draft rule set. Requires admin/owner.
Parses `definition` against `IntakeRuleSet` (422 on shape errors).
There is at most **one open draft per inbox**: this cuts a new version only when the newest revision is no longer editable (deployed, archived, or a `superseded` draft) — otherwise it overwrites the open draft in place and answers 200 rather than 201, because nothing was created. A definition whose hash matches the latest revision writes nothing at all (also 200, the existing revision returned). See `inbox_service.push_revision` for why saving is not a version-cutting operation.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | -------- | -------- | ----------- |
| definition | `object` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 201 | Successful Response | `InboxRevisionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/inboxes/{inbox_id}/revisions/{revision_id}`
[Section titled “GET /api/orgs/{org\_id}/inboxes/{inbox\_id}/revisions/{revision\_id}”](#get-apiorgsorg_idinboxesinbox_idrevisionsrevision_id)
Get Revision
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| revision\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `InboxRevisionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/inboxes/{inbox_id}/revisions/{revision_id}`
[Section titled “DELETE /api/orgs/{org\_id}/inboxes/{inbox\_id}/revisions/{revision\_id}”](#delete-apiorgsorg_idinboxesinbox_idrevisionsrevision_id)
Discard Revision
Discard a draft revision — the open draft, or a `superseded` one the Rules screen surfaces as leftover debris. A `deployed` or `archived` revision is refused (409): those are history, and rolling back is how you return to one.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| revision\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/revisions/{revision_id}/deploy`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/revisions/{revision\_id}/deploy”](#post-apiorgsorg_idinboxesinbox_idrevisionsrevision_iddeploy)
Deploy Revision
Validate + deploy a draft revision. Requires admin/owner.
Swaps `Inbox.current_revision_id` and archives the previously-deployed revision in one commit — atomic from any reader’s point of view, and never touches an already-written `inbox_messages.revision_id`, so a message mid-evaluation when this runs finishes on the revision it started with.
Also relabels any other still-`draft` revision older than this one as `superseded` (see `inbox_service.deploy_revision`), so the Rules screen can tell a stale, never-deployed draft apart from one that’s genuinely ahead of what’s live.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| revision\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `DeployInboxRevisionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/revisions/{revision_id}/rollback`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/revisions/{revision\_id}/rollback”](#post-apiorgsorg_idinboxesinbox_idrevisionsrevision_idrollback)
Rollback Revision
Restore a previously-deployed (now `archived`) revision as current.
Not a re-deploy: no new version is created and no re-validation is forced — the revision was validated when it first went live, and version numbers are never renumbered (contract “Versioned revisions” decision). Requires admin/owner.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| revision\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `DeployInboxRevisionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inboxes/{inbox_id}/revisions/{revision_id}/validate`
[Section titled “POST /api/orgs/{org\_id}/inboxes/{inbox\_id}/revisions/{revision\_id}/validate”](#post-apiorgsorg_idinboxesinbox_idrevisionsrevision_idvalidate)
Validate Revision
Deep-validate a draft: every route/prompt reference resolves.
Runs PR 3’s typed rule vocabulary over the draft definition without dispatching anything — no envelope is evaluated, no chat or process run is created. Never mutates the revision; `deploy` re-runs this and refuses on the same errors.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| inbox\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| revision\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `ValidateRevisionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Intake
> REST API reference for intake.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `POST https://api.agentdepot.org/api/intake/email/{org_slug}/{inbox_slug}`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### POST `/api/intake/email/{org_slug}/{inbox_slug}`
[Section titled “POST /api/intake/email/{org\_slug}/{inbox\_slug}”](#post-apiintakeemailorg_sluginbox_slug)
Ingest Email Codeless
Codeless (public) email address: `in.{inbox}.{org}@zone`.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | -------- | -------- | ----------- |
| inbox\_slug | path | `string` | yes | |
| org\_slug | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 201 | Successful Response | `IntakeIngestResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/intake/email/{org_slug}/{inbox_slug}/{code}`
[Section titled “POST /api/intake/email/{org\_slug}/{inbox\_slug}/{code}”](#post-apiintakeemailorg_sluginbox_slugcode)
Ingest Email Coded
Coded email address: `in.{inbox}.{org}.{code}@zone`.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | -------- | -------- | ----------- |
| code | path | `string` | yes | |
| inbox\_slug | path | `string` | yes | |
| org\_slug | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 201 | Successful Response | `IntakeIngestResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/intake/hook/{address_token}`
[Section titled “POST /api/intake/hook/{address\_token}”](#post-apiintakehookaddress_token)
Ingest Webhook
`POST /api/intake/hook/{address_token}` — the token-only path shape.
Still the advertised URL where no agent zone is configured (local dev), and kept live everywhere for senders configured before the org-host shape existed. The token is the same credential either way, so nothing is weaker here than on the host route.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ---- | -------- | -------- | ----------- |
| address\_token | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 201 | Successful Response | `IntakeIngestResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/intake/run/{run_token}`
[Section titled “GET /api/intake/run/{run\_token}”](#get-apiintakerunrun_token)
Run Status
`GET /api/intake/run/{run_token}` — the token-only path shape.
The token is the same credential either way, so nothing is weaker here than on the host route.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | -------- | -------- | ----------- |
| run\_token | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `IntakeRunResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Composio Integrations
> REST API reference for composio integrations.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/integrations/composio/callback`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/integrations/composio/callback`
[Section titled “GET /api/integrations/composio/callback”](#get-apiintegrationscomposiocallback)
Composio Callback
Provider OAuth redirect target: verify, flip ACTIVE, bounce to the app.
Trusts only the signed `state` (binds `connection_id` ↔ `org_id`). The connection id itself is read from the (org-scoped) row, never the query, so a forged callback cannot activate another org’s connection. Always redirects to the app — the authenticated status poll remains the source of truth.
**Parameters**
| Name | In | Type | Required | Description |
| ----- | ----- | -------- | -------- | ----------- |
| state | query | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/integrations/composio/connect`
[Section titled “POST /api/orgs/{org\_id}/integrations/composio/connect”](#post-apiorgsorg_idintegrationscomposioconnect)
Connect Toolkit
Initiate a Composio connection for a toolkit (any member — see #260).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------- | -------- | -------- | ----------- |
| credentials | `object` | no | |
| scope | `string` | no | |
| toolkit\_slug | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ConnectResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/integrations/composio/connect-fields`
[Section titled “GET /api/orgs/{org\_id}/integrations/composio/connect-fields”](#get-apiorgsorg_idintegrationscomposioconnect-fields)
Get Connect Fields
Return the credential fields required to connect a toolkit (any member — see #260).
For API-key / basic / bearer toolkits (`requires_credentials=True`) this returns the list of fields the user must supply before calling `/connect`. OAuth toolkits return an empty `fields` list with `requires_credentials=False` — no form is needed, the redirect handles it.
Returns 404 when Composio has no enabled auth config for the toolkit, 502 for unexpected upstream failures.
**Parameters**
| Name | In | Type | Required | Description |
| ------------- | ----- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| toolkit\_slug | query | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `ConnectSchemaResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/integrations/composio/connections`
[Section titled “GET /api/orgs/{org\_id}/integrations/composio/connections”](#get-apiorgsorg_idintegrationscomposioconnections)
List Connections
List the org’s Composio connections and their lifecycle status.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------------------------------------------------ |
| 200 | Successful Response | `agentdepot_api__routers__integrations_composio__ConnectionListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/integrations/composio/connections/{integration_id}`
[Section titled “DELETE /api/orgs/{org\_id}/integrations/composio/connections/{integration\_id}”](#delete-apiorgsorg_idintegrationscomposioconnectionsintegration_id)
Disconnect Toolkit
Disconnect a toolkit: delete the Composio account and the row (any member — see #260).
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| integration\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/integrations/composio/connections/{integration_id}/reinitiate`
[Section titled “POST /api/orgs/{org\_id}/integrations/composio/connections/{integration\_id}/reinitiate”](#post-apiorgsorg_idintegrationscomposioconnectionsintegration_idreinitiate)
Reinitiate Connection
Restart OAuth for a stuck PENDING/ERROR connection (any member — see #260).
The signed callback state expires after `_STATE_MAX_AGE_SECONDS` (30 minutes) and the original `redirect_url` isn’t persisted, so a user who abandons the OAuth flow (or hits a connect error) has no way to resume — this mints a fresh signed state and re-initiates the same toolkit/auth config under the row’s original identity. 409 if the row isn’t PENDING or ERROR (e.g. already ACTIVE).
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| integration\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ConnectResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/integrations/composio/connections/{integration_id}/status`
[Section titled “GET /api/orgs/{org\_id}/integrations/composio/connections/{integration\_id}/status”](#get-apiorgsorg_idintegrationscomposioconnectionsintegration_idstatus)
Get Connection Status
Poll Composio for a connection’s status and reconcile the row.
The authenticated source of truth the frontend polls after redirecting the user to OAuth — independent of whether the public callback fired.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| integration\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `ConnectionStatusResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/integrations/composio/toolkits`
[Section titled “GET /api/orgs/{org\_id}/integrations/composio/toolkits”](#get-apiorgsorg_idintegrationscomposiotoolkits)
List Toolkits
List only the toolkits the org’s Composio account has an auth config for.
Instead of returning the entire Composio catalog, this fetches the set of auth configs the account has registered (e.g. `outlook`, `microsoft_teams`), restricts the catalog to those slugs, and annotates each with the org’s connection state. Any configured toolkit not found in the catalog page is included as a minimal entry so nothing is silently dropped.
The Composio catalog (`list_auth_configs` + `list_toolkits`) is cached in-process (see `services/composio_catalog.py`) because it is account-global and changes rarely. The `search` filter is applied in Python against the cached unfiltered data so caching does not break search. Per-org connection state is always fetched live from the DB.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| search | query | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ToolkitListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Invitations
> REST API reference for invitations.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/invitations/{token}`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/invitations/{token}`
[Section titled “GET /api/invitations/{token}”](#get-apiinvitationstoken)
Get Invitation
Resolve the invitation behind an email link’s token.
Public — see the module docstring for why, and for what it is safe to return. `status` is reported as-is (`pending` / `accepted` / `revoked`) rather than 404-ing on a spent invitation, so the page can say “you’ve already joined, sign in” instead of “this link is broken”. A token that has been rotated away from — or that never matched anything — gets a plain 404, which is also what a mid-rollout row created by the previous release gets: it has a token, but one that was never mailed to anybody.
**Parameters**
| Name | In | Type | Required | Description |
| ----- | ---- | -------- | -------- | -------------------------------------------------- |
| token | path | `string` | yes | The opaque secret from the invitation email’s link |
**Responses**
| Status | Description | Body |
| ------ | -------------------------------- | -------------------------- |
| 200 | Successful Response | `PublicInvitationResponse` |
| 404 | No invitation matches this token | |
| 422 | Validation Error | `HTTPValidationError` |
# Knowledge
> REST API reference for knowledge.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/knowledge`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/knowledge`
[Section titled “GET /api/orgs/{org\_id}/knowledge”](#get-apiorgsorg_idknowledge)
List Knowledge Bases
List knowledge bases for the organization, with source/chunk counts.
Omit `limit` to get every KB; `total` is the full count either way.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ------------------------------------------------------------------ |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | query | `string` | no | Filter by project id, or ‘none’ for uncategorized knowledge bases. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `Page_KnowledgeBaseResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge`
[Section titled “POST /api/orgs/{org\_id}/knowledge”](#post-apiorgsorg_idknowledge)
Create Knowledge Base
Create a new knowledge base. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 201 | Successful Response | `KnowledgeBaseResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/knowledge/{kb_id}`
[Section titled “GET /api/orgs/{org\_id}/knowledge/{kb\_id}”](#get-apiorgsorg_idknowledgekb_id)
Get Knowledge Base
Get a knowledge base’s detail (with source/chunk counts).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `KnowledgeBaseResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/knowledge/{kb_id}`
[Section titled “PATCH /api/orgs/{org\_id}/knowledge/{kb\_id}”](#patch-apiorgsorg_idknowledgekb_id)
Update Knowledge Base
Rename / re-describe a knowledge base. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `KnowledgeBaseResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/knowledge/{kb_id}`
[Section titled “DELETE /api/orgs/{org\_id}/knowledge/{kb\_id}”](#delete-apiorgsorg_idknowledgekb_id)
Delete Knowledge Base
Soft-delete a knowledge base. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/knowledge/{kb_id}/documents`
[Section titled “GET /api/orgs/{org\_id}/knowledge/{kb\_id}/documents”](#get-apiorgsorg_idknowledgekb_iddocuments)
List Documents
The knowledge base’s written documents as a flat page tree — build the tree from `parent_id`; siblings arrive in display order. Any member.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `KnowledgeDocumentTreeItem[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/documents`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/documents”](#post-apiorgsorg_idknowledgekb_iddocuments)
Create Document
Create a markdown document from the editor. Indexed like any source; an empty one is simply marked ready with nothing indexed.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | --------------- | -------- | ----------- |
| content | `string` | no | |
| parent\_id | `string (uuid)` | no | |
| title | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 201 | Successful Response | `KnowledgeDocumentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/knowledge/{kb_id}/documents/{source_id}`
[Section titled “GET /api/orgs/{org\_id}/knowledge/{kb\_id}/documents/{source\_id}”](#get-apiorgsorg_idknowledgekb_iddocumentssource_id)
Get Document
Read a written document’s markdown. Any member may read; only admins save.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| source\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `KnowledgeDocumentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/knowledge/{kb_id}/documents/{source_id}`
[Section titled “PUT /api/orgs/{org\_id}/knowledge/{kb\_id}/documents/{source\_id}”](#put-apiorgsorg_idknowledgekb_iddocumentssource_id)
Update Document
Save a document. Re-indexes only when the markdown changed.
The row is locked for the compare-and-write, so two concurrent saves against the same `base_version` cannot both succeed.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| source\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------- | -------- | -------- | ----------- |
| base\_version | `string` | no | |
| content | `string` | no | |
| title | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `KnowledgeDocumentResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/documents/{source_id}/move`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/documents/{source\_id}/move”](#post-apiorgsorg_idknowledgekb_iddocumentssource_idmove)
Move Document
Move a document under another one (or to the top level) and/or reorder it among its siblings. Its sub-documents move with it. Never re-indexes: where a page sits is not part of its chunks.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| source\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | --------------- | -------- | ----------- |
| parent\_id | `string (uuid)` | no | |
| position | `number` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `KnowledgeDocumentTreeItem` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/search`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/search”](#post-apiorgsorg_idknowledgekb_idsearch)
Search Knowledge Base
Test-search a single knowledge base.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | --------- | -------- | ----------- |
| limit | `integer` | no | |
| query | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__knowledge__SearchResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/knowledge/{kb_id}/sources`
[Section titled “GET /api/orgs/{org\_id}/knowledge/{kb\_id}/sources”](#get-apiorgsorg_idknowledgekb_idsources)
List Sources
List a knowledge base’s sources, including sync status.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `KnowledgeSourceResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/knowledge/{kb_id}/sources/{source_id}`
[Section titled “DELETE /api/orgs/{org\_id}/knowledge/{kb\_id}/sources/{source\_id}”](#delete-apiorgsorg_idknowledgekb_idsourcessource_id)
Delete Source
Delete a source. Chunks cascade via the DB foreign key.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| source\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/knowledge/{kb_id}/sources/{source_id}/chunks`
[Section titled “GET /api/orgs/{org\_id}/knowledge/{kb\_id}/sources/{source\_id}/chunks”](#get-apiorgsorg_idknowledgekb_idsourcessource_idchunks)
List Source Chunks
Paginated chunk preview for a source, ordered by position.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ----- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| source\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ChunkListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/sources/{source_id}/resync`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/sources/{source\_id}/resync”](#post-apiorgsorg_idknowledgekb_idsourcessource_idresync)
Resync Source
Reset a source to PENDING and re-trigger ingestion.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| source\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `KnowledgeSourceResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/sources/file`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/sources/file”](#post-apiorgsorg_idknowledgekb_idsourcesfile)
Create File Sources
Stage one or more previously-uploaded temp files into the knowledge base.
Mirrors `chat_files.py::attach_chat_files`: each `file_ref` points at a file already sitting in the temp S3 bucket (via a presigned upload); this endpoint downloads it, re-uploads it to permanent storage under the KB’s namespace, creates one `FILE` source per file (title = filename), and pushes `KNOWLEDGE_SOURCE_ADDED` for each.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | ----------------------------------------------- | -------- | ----------- |
| file\_refs | `agentdepot_api__routers__knowledge__FileRef[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 201 | Successful Response | `KnowledgeSourceResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/sources/text`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/sources/text”](#post-apiorgsorg_idknowledgekb_idsourcestext)
Create Text Source
Add manually-entered text as a source.
The content is staged to permanent S3 as `text/markdown` (mirroring the file path — the ingestion workflow downloads and extracts it the same way) rather than stored inline on the row.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------- | -------- | -------- | ----------- |
| content | `string` | yes | |
| title | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 201 | Successful Response | `KnowledgeSourceResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/sources/url`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/sources/url”](#post-apiorgsorg_idknowledgekb_idsourcesurl)
Create Url Source
Add a website URL as a source (title starts as the URL; the ingestion workflow may refine it once the page is fetched).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| url | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 201 | Successful Response | `KnowledgeSourceResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/knowledge/{kb_id}/sync-configs`
[Section titled “GET /api/orgs/{org\_id}/knowledge/{kb\_id}/sync-configs”](#get-apiorgsorg_idknowledgekb_idsync-configs)
List Sync Configs
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `SyncConfigResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/sync-configs`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/sync-configs”](#post-apiorgsorg_idknowledgekb_idsync-configs)
Create Sync Config
Connect a remote collection to the KB and trigger the initial sync.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------- | --------------- | -------- | ----------- |
| collection\_id | `string` | yes | |
| collection\_name | `string` | yes | |
| integration\_id | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `SyncConfigResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/knowledge/{kb_id}/sync-configs/{config_id}`
[Section titled “DELETE /api/orgs/{org\_id}/knowledge/{kb\_id}/sync-configs/{config\_id}”](#delete-apiorgsorg_idknowledgekb_idsync-configsconfig_id)
Delete Sync Config
Disconnect a collection. Its synced sources (and chunks) are deleted.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| config\_id | path | `string (uuid)` | yes | |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/{kb_id}/sync-configs/{config_id}/sync`
[Section titled “POST /api/orgs/{org\_id}/knowledge/{kb\_id}/sync-configs/{config\_id}/sync”](#post-apiorgsorg_idknowledgekb_idsync-configsconfig_idsync)
Trigger Sync
Manually trigger a sync of one connected collection.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| config\_id | path | `string (uuid)` | yes | |
| kb\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SyncConfigResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/knowledge/integrations/{integration_id}/collections`
[Section titled “GET /api/orgs/{org\_id}/knowledge/integrations/{integration\_id}/collections”](#get-apiorgsorg_idknowledgeintegrationsintegration_idcollections)
List Remote Collections
List the syncable collections of a knowledge-capable connection.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| integration\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `RemoteCollectionResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/knowledge/search`
[Section titled “POST /api/orgs/{org\_id}/knowledge/search”](#post-apiorgsorg_idknowledgesearch)
Search Org Knowledge
Test-search across every knowledge base in the org (no `kb_id` scope).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | --------- | -------- | ----------- |
| limit | `integer` | no | |
| query | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__knowledge__SearchResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Library
> REST API reference for library.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/library`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/library`
[Section titled “GET /api/orgs/{org\_id}/library”](#get-apiorgsorg_idlibrary)
List Library
Browse or search the organization’s files and artifacts.
**Without `q`** this is a plain newest-first listing: exact `total`, real SQL paging to any depth.
**With `q`** it is a hybrid search — trigram matching on names and artifact content, unioned with cosine similarity over embeddings, fused and ranked. Note that `total` then means *the number of ranked candidates this endpoint will return*, not the number of rows in the org that match; it is bounded by the per-channel candidate caps, and it moves as the query is typed. Clients should not render it as a corpus count.
Rows created before the embedding sweep’s epoch have no vector, so they are reachable by name but not semantically. Semantic matching is document-level: the sweep embeds a bounded head of each document, so a term deep inside a large PDF may not match.
`type` narrows to one backing table on its own: a MIME family excludes artifacts and an artifact type excludes uploads, because a facet chip means “only these”, not “these plus everything else”. Pass `kind` to be explicit.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ----- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| agent\_id | query | `string (uuid)` | no | |
| chat\_id | query | `string (uuid)` | no | |
| is\_dry\_run | query | `boolean` | no | true = only dry-run drafts; false = exclude them. Omit to include everything (drafts flagged `is_dry_run`). |
| kind | query | `"file"` \| `"artifact"` | no | Restrict to uploads or agent artifacts |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| q | query | `string` | no | Hybrid search query |
| type | query | `string` | no | Facet token: a MIME family (image/document/spreadsheet/presentation/text/data/archive/other) or an artifact type (text/code/markdown/json/file). |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_LibraryItem_` |
| 422 | Validation Error | `HTTPValidationError` |
# Members
> REST API reference for members.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/members`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/members`
[Section titled “GET /api/orgs/{org\_id}/members”](#get-apiorgsorg_idmembers)
List Members
List all members of the organization.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `MemberListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/members/{member_id}`
[Section titled “GET /api/orgs/{org\_id}/members/{member\_id}”](#get-apiorgsorg_idmembersmember_id)
Get Member
Get details of a specific member.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| member\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `MemberResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/members/{member_id}`
[Section titled “PATCH /api/orgs/{org\_id}/members/{member\_id}”](#patch-apiorgsorg_idmembersmember_id)
Update Member
Update a member’s role.
Requires ADMIN or OWNER role. Only OWNER can promote to ADMIN or OWNER. Cannot change your own role. Cannot demote the last OWNER.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| member\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| role | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `MemberResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/members/{member_id}`
[Section titled “DELETE /api/orgs/{org\_id}/members/{member\_id}”](#delete-apiorgsorg_idmembersmember_id)
Remove Member
Remove a member from the organization.
Requires ADMIN or OWNER role. Cannot remove yourself — use `POST /leave` instead. Cannot remove the last OWNER.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ---- | --------------- | -------- | ----------- |
| member\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/members/invitations`
[Section titled “GET /api/orgs/{org\_id}/members/invitations”](#get-apiorgsorg_idmembersinvitations)
List Invitations
List all pending invitations for the organization.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `InvitationResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/members/invitations/{invitation_id}`
[Section titled “PATCH /api/orgs/{org\_id}/members/invitations/{invitation\_id}”](#patch-apiorgsorg_idmembersinvitationsinvitation_id)
Update Invitation
Change the role a pending invitation will grant on redemption.
The create-organization wizard sends every invite as MEMBER and lets the owner adjust roles afterwards, so this is the ordinary path rather than a correction: by then the invitation row already exists.
Requires ADMIN or OWNER; only an OWNER may raise an invitation to ADMIN or OWNER, matching `invite_member`.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ---- | --------------- | -------- | ----------- |
| invitation\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| role | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `InvitationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/members/invitations/{invitation_id}`
[Section titled “DELETE /api/orgs/{org\_id}/members/invitations/{invitation\_id}”](#delete-apiorgsorg_idmembersinvitationsinvitation_id)
Revoke Invitation
Revoke a pending invitation.
Requires ADMIN or OWNER role. Only pending invitations can be revoked.
Terminal and irreversible: the row keeps its token but can never return to `pending`, so the emailed link is dead for good. Re-inviting the address mints a fresh invitation with a fresh token (`invite_member`) — the leaked link is never resurrected, which is the whole point of revoking.
**Parameters**
| Name | In | Type | Required | Description |
| -------------- | ---- | --------------- | -------- | ----------- |
| invitation\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/members/invite`
[Section titled “POST /api/orgs/{org\_id}/members/invite”](#post-apiorgsorg_idmembersinvite)
Invite Member
Add a user to the organization by email.
Requires ADMIN or OWNER role. If the user already has an account, they are added directly and a MemberResponse is returned. If they don’t have an account yet, a pending invitation is created and an InvitationResponse is returned (HTTP 201). When the user signs up, the invitation is redeemed automatically.
Re-inviting an address whose earlier invitation was revoked (or accepted, then unwound) creates a **new** invitation with a **new** token rather than reviving the old row — see the comment at the insert. A second *pending* invitation for the same address is a 409.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | ---------------- | -------- | ----------- |
| email | `string (email)` | yes | |
| role | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------------------- |
| 201 | Successful Response | `MemberResponse` \| `InvitationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/members/leave`
[Section titled “POST /api/orgs/{org\_id}/members/leave”](#post-apiorgsorg_idmembersleave)
Leave Org
Leave the organization.
Requires membership. The one case this refuses is the last OWNER: an org must never end up with zero owners, so that member has to promote someone else to OWNER (`PATCH /{member_id}`) — or delete the org — before they can leave. Every other member, including an ADMIN or a non-last OWNER, can always leave.
This is what `remove_member` points a self-removal attempt at: removing yourself via `DELETE /{member_id}` is refused outright rather than silently redirected, since a member removing *themselves* is a different action (no ADMIN/OWNER permission check, no “cannot remove the last OWNER” — that becomes “cannot leave”) from an admin removing someone else.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# Models
> REST API reference for models.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/models/catalog`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/models/catalog`
[Section titled “GET /api/models/catalog”](#get-apimodelscatalog)
Get Model Catalog
Return the enabled chat models, in display order.
Replaces the model array that used to be hardcoded in the web app. Only enabled rows are exposed — a model switched off in the admin panel must disappear from every picker, and is rejected server-side regardless. Embedding models share the catalog (same pricing shape, same admin surface) but are never conversational, so they are filtered out here.
`org_id` is **optional and membership-checked**: with it the caller gets that org’s merged view, so a model the org offers itself appears in its picker and a model it re-routed shows its own route. Without it, the platform’s — which is what every caller sent before PR 4 and still means the same thing.
Optional rather than required because this endpoint is user-scoped and mounted outside `/orgs/{org_id}`; making it required would break every existing caller to serve a case only enterprise orgs have. A caller passing an org it does not belong to gets 403 rather than a quietly platform-only answer — a silent downgrade here would show a picker missing the org’s own models with no indication why.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ----------- |
| org\_id | query | `string (uuid)` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CatalogModel[]` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/models/pricing`
[Section titled “GET /api/models/pricing”](#get-apimodelspricing)
Get Model Pricing
Return LLM model pricing in per-1-million-token units.
Served from the platform model catalog — the same numbers runs are billed on, so what a picker shows and what a chat costs cannot drift. A model whose catalog row has no input/output price is omitted; admins fill those in (or sync them from the LiteLLM dataset) in the admin panel.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
# Org Inference Routes
> REST API reference for org inference routes.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/inference-routes`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/inference-routes`
[Section titled “GET /api/orgs/{org\_id}/inference-routes”](#get-apiorgsorg_idinference-routes)
List Org Routes
The routes this org authored. Platform routes are not listed here.
Deliberately not the dispatchable list: the platform’s routes are configured (keys, endpoint overrides, on/off) under `/settings/ai/routes`, and mixing rows an org may delete with rows it may not into one list is how a delete button ends up next to something it cannot act on.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------- |
| 200 | Successful Response | `OrgInferenceRouteListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/inference-routes`
[Section titled “POST /api/orgs/{org\_id}/inference-routes”](#post-apiorgsorg_idinference-routes)
Create Org Route
Author a route pointing at an endpoint this org runs.
Requires ADMIN or OWNER role, and `byo_keys`.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| base\_url | `string` | yes | The endpoint’s URL. Must be reachable from the public internet. |
| dialect | `"anthropic"` \| `"openai"` \| `"voyage"` | yes | The wire format the endpoint speaks — not the vendor. |
| label | `string` | yes | |
| slug | `string` | yes | Lowercase kebab-case. Globally unique across every organization, because it is also the name of this route’s credential setting. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 201 | Successful Response | `OrgInferenceRouteResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/inference-routes/{route_id}`
[Section titled “PATCH /api/orgs/{org\_id}/inference-routes/{route\_id}”](#patch-apiorgsorg_idinference-routesroute_id)
Update Org Route
Change a route’s label, endpoint or on/off state.
`slug` and `dialect` are not editable. The slug is stored on offerings and in the credential setting’s own name, so renaming it is a data migration rather than an edit; the dialect decides which SDK is constructed, so changing it under an offering would send one wire format to an endpoint speaking another.
Switching a route **off** is allowed with offerings still pinned to it, and that is the difference from the admin surface: this row is the org’s own, a disabled route hard-fails at dispatch naming itself, and refusing here would remove the one control an org has when its endpoint is misbehaving.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| route\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | --------- | -------- | ----------- |
| base\_url | `string` | no | |
| enabled | `boolean` | no | |
| label | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `OrgInferenceRouteResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/inference-routes/{route_id}`
[Section titled “DELETE /api/orgs/{org\_id}/inference-routes/{route\_id}”](#delete-apiorgsorg_idinference-routesroute_id)
Delete Org Route
Remove a route.
Refused with 409 while any offering still pins it. Not a cascade: deleting the endpoint out from under an offering would leave a model that resolves to nothing, and the first anyone hears of it is a failed turn. The FK is `RESTRICT` and would refuse anyway — this exists to say *what* is pinned, which the database’s own error cannot.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| route\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# Org Integrations
> REST API reference for org integrations.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/integrations/check-slug`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/integrations/check-slug`
[Section titled “GET /api/orgs/{org\_id}/integrations/check-slug”](#get-apiorgsorg_idintegrationscheck-slug)
Check Slug Availability
Check whether a connection slug is valid and not yet taken in this org.
Used by the create/edit dialogs for live validation. Pass `exclude_id` to ignore the integration being edited when checking uniqueness.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ----------- |
| exclude\_id | query | `string (uuid)` | no | |
| org\_id | path | `string (uuid)` | yes | |
| slug | query | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SlugCheckResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/integrations/configured`
[Section titled “GET /api/orgs/{org\_id}/integrations/configured”](#get-apiorgsorg_idintegrationsconfigured)
List Configured Integrations
List the organization’s configured integrations.
Requires membership in the organization. Secret params are redacted.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------------- |
| 200 | Successful Response | `ConfiguredIntegrationListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/integrations/configured`
[Section titled “POST /api/orgs/{org\_id}/integrations/configured”](#post-apiorgsorg_idintegrationsconfigured)
Create Configured Integration
Create a new configured integration.
Any org member may create integrations for now — see issue #260. Validates params against the registry.
For MCP\_SERVER integrations the tool-definition cache is invalidated after creation so that the next tool-discovery read fetches fresh tool metadata from the upstream server.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------- | -------- | -------- | ----------- |
| app\_name | `string` | no | |
| description | `string` | no | |
| integration\_type | `string` | yes | |
| name | `string` | yes | |
| params | `object` | yes | |
| scope | `string` | no | |
| slug | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------- |
| 201 | Successful Response | `ConfiguredIntegrationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/integrations/configured/{integration_id}`
[Section titled “GET /api/orgs/{org\_id}/integrations/configured/{integration\_id}”](#get-apiorgsorg_idintegrationsconfiguredintegration_id)
Get Configured Integration
Get a single configured integration.
Requires membership in the organization. Secret params are redacted.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| integration\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------- |
| 200 | Successful Response | `ConfiguredIntegrationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/integrations/configured/{integration_id}`
[Section titled “PATCH /api/orgs/{org\_id}/integrations/configured/{integration\_id}”](#patch-apiorgsorg_idintegrationsconfiguredintegration_id)
Update Configured Integration
Update a configured integration.
Any org member may update integrations for now — see issue #260. A PRIVATE row owned by someone else 404s. Params are merged (partial update).
For MCP\_SERVER integrations the tool-definition cache is invalidated after a successful update so that stale metadata (e.g. from a URL or credential change) is not served to agents.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| integration\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| app\_name | `string` | no | |
| description | `string` | no | |
| name | `string` | no | |
| params | `object` | no | |
| slug | `string` | no | |
| status | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------- |
| 200 | Successful Response | `ConfiguredIntegrationResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/integrations/configured/{integration_id}`
[Section titled “DELETE /api/orgs/{org\_id}/integrations/configured/{integration\_id}”](#delete-apiorgsorg_idintegrationsconfiguredintegration_id)
Delete Configured Integration
Delete a configured integration.
Any org member may delete integrations for now — see issue #260. A PRIVATE row owned by someone else 404s.
**Parameters**
| Name | In | Type | Required | Description |
| --------------- | ---- | --------------- | -------- | ----------- |
| integration\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/integrations/types`
[Section titled “GET /api/orgs/{org\_id}/integrations/types”](#get-apiorgsorg_idintegrationstypes)
List Integration Types
List available integration types from the registry.
Requires membership in the organization.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `IntegrationTypeListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Org Models
> REST API reference for org models.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/feature-models`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/feature-models`
[Section titled “GET /api/orgs/{org\_id}/feature-models”](#get-apiorgsorg_idfeature-models)
List Org Feature Models
Every org-facing feature, in the platform admin’s own display order.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `OrgFeatureModelResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/feature-models/{feature_id}`
[Section titled “PATCH /api/orgs/{org\_id}/feature-models/{feature\_id}”](#patch-apiorgsorg_idfeature-modelsfeature_id)
Set Org Feature Model
Set or clear this org’s override for one org-facing feature.
`model: null` clears it, and the feature falls back to the platform’s own choice on the very next call — no cache to invalidate, matching `resolve_feature_model`’s own reasoning for reading settings live.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| feature\_id | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----- | -------- | -------- | ----------- |
| model | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `OrgFeatureModelResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/model-offerings`
[Section titled “GET /api/orgs/{org\_id}/model-offerings”](#get-apiorgsorg_idmodel-offerings)
List Org Offerings
This org’s own offerings, and how many more its plan allows.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `OrgModelOfferingListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/model-offerings`
[Section titled “POST /api/orgs/{org\_id}/model-offerings”](#post-apiorgsorg_idmodel-offerings)
Create Org Offering
Offer a model on one of this org’s routes — the platform’s, or its own.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------------- | -------------------- | -------- | ----------- |
| cache\_write\_price\_per\_1m | `number` \| `string` | no | |
| cached\_input\_price\_per\_1m | `number` \| `string` | no | |
| custom | `CustomModelSpec` | no | |
| enabled | `boolean` | no | |
| input\_price\_per\_1m | `number` \| `string` | no | |
| model\_key | `string` | yes | |
| output\_price\_per\_1m | `number` \| `string` | no | |
| remote\_model\_id | `string` | no | |
| route\_slug | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 201 | Successful Response | `OrgModelOfferingResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/model-offerings/{offering_id}`
[Section titled “PATCH /api/orgs/{org\_id}/model-offerings/{offering\_id}”](#patch-apiorgsorg_idmodel-offeringsoffering_id)
Update Org Offering
Re-route, re-price or switch off one of this org’s offerings.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| offering\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------------- | -------------------- | -------- | ----------- |
| cache\_write\_price\_per\_1m | `number` \| `string` | no | |
| cached\_input\_price\_per\_1m | `number` \| `string` | no | |
| custom | `CustomModelSpec` | no | |
| enabled | `boolean` | no | |
| input\_price\_per\_1m | `number` \| `string` | no | |
| output\_price\_per\_1m | `number` \| `string` | no | |
| remote\_model\_id | `string` | no | |
| route\_slug | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `OrgModelOfferingResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/model-offerings/{offering_id}`
[Section titled “DELETE /api/orgs/{org\_id}/model-offerings/{offering\_id}”](#delete-apiorgsorg_idmodel-offeringsoffering_id)
Delete Org Offering
Remove one of this org’s offerings.
The model itself is untouched — it is the platform’s, and agents pinned to it fall back to the platform’s offering on the next turn rather than losing the model.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| offering\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/model-offerings/{offering_id}/dependents`
[Section titled “GET /api/orgs/{org\_id}/model-offerings/{offering\_id}/dependents”](#get-apiorgsorg_idmodel-offeringsoffering_iddependents)
List Offering Dependents
What in this org would stop working if this offering were removed.
Its own read rather than a field on the offering, because it is a question asked once — at the moment somebody clicks remove — and answering it on every list would put two extra queries per row behind a page that mostly does not need them.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| offering\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `OfferingDependentsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/model-offerings/{offering_id}/test`
[Section titled “POST /api/orgs/{org\_id}/model-offerings/{offering\_id}/test”](#post-apiorgsorg_idmodel-offeringsoffering_idtest)
Test Org Offering
Actually call this offering’s endpoint and report what happened.
Everything else about a custom offering is declarative — the org states a URL, a dialect, a model id and a window, and nothing disagrees until an agent takes a turn and the turn fails inside a chat, days later, looking like a bad agent. This is the org asking directly, while they are still on the page that caused it.
**A failed test is a 200.** The request succeeded; the endpoint is what did not. Returning 4xx/5xx would make “your endpoint is down” indistinguishable from “your session expired” to every client that branches on status, and would throw away the per-step detail that is the entire value here.
The verdict is stored on the row (`last_tested_at` / `last_test_ok`) but gates nothing: an endpoint that answered a minute ago can be down now, and refusing dispatch on a remembered verdict would substitute a stale fact for a live one.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| offering\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `OfferingTestResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/model-offerings/platform-rates`
[Section titled “GET /api/orgs/{org\_id}/model-offerings/platform-rates”](#get-apiorgsorg_idmodel-offeringsplatform-rates)
List Platform Model Rates
The platform’s own rates, keyed by `(model, route)` — what a blank inherits.
Declared **above** the `/{offering_id}` routes so a literal path segment is never a candidate offering id.
Read from the platform’s uncollapsed catalog rows (`org_id` omitted on purpose: the merged view would echo this org’s own offering back and call its blank a platform rate). Chat rows only — an org neither picks nor authors an embedding model, so there is no offering here to inherit one.
**Gated on `byo_keys` as well as admin**, unlike the list it accompanies. These are the rates the platform pays providers. For an org that authors its own offerings they are the rate its own calls bill at — a number it has to be able to see, and the entitlement that lets dollars leave the API at all (`spend_context`). For everyone else they are our cost basis and nothing the org could act on, which is what the spend guard exists to keep in.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------- |
| 200 | Successful Response | `PlatformModelRateListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Org Settings
> REST API reference for org settings.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/model-config`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/model-config`
[Section titled “GET /api/orgs/{org\_id}/model-config”](#get-apiorgsorg_idmodel-config)
Get Model Config
Return the org’s model policy (allowlists + defaults) per scope.
Readable by any org member — it drives client-side model-picker filtering and default selection. The real enforcement is server-side (agent CRUD + chat send), so this endpoint carries no secrets and no budget data.
`disabled_providers` reports the toggles **as the server will apply them**, which for an org without `byo_keys` is none at all: the toggle governs a provider that org does not own and is skipped at `ensure_model_allowed_for_org` step 5. Reporting a stored-but-inert value would grey out models the picker would then have accepted — a hint that disagrees with the gate is worse than no hint, because the disagreement only shows up as models mysteriously missing.
`model_routes` is this org’s answer to “where does each model’s calls go and whose key pays” — populated only when the org holds `byo_keys`, null otherwise. This is the surface `CatalogModel` (`GET /api/models/catalog`) names as the intended home for that information, because that endpoint is platform-wide and has no entitlement to check; this one is org-scoped and does.
Requires membership.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ModelConfigResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/settings/ai`
[Section titled “GET /api/orgs/{org\_id}/settings/ai”](#get-apiorgsorg_idsettingsai)
Get Ai Settings
Get AI provider settings for the organization (secrets redacted).
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `AIProviderSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/settings/ai`
[Section titled “PATCH /api/orgs/{org\_id}/settings/ai”](#patch-apiorgsorg_idsettingsai)
Update Ai Settings
Update AI provider settings for the organization.
Requires ADMIN or OWNER role. Only fields present in the request body are updated; omitted fields are left unchanged.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------------- | ------------------------------------- | -------- | ----------- |
| ai\_gateway\_account\_id | `string` | no | |
| ai\_gateway\_gateway\_id | `string` | no | |
| ai\_gateway\_log\_payloads | `string` | no | |
| ai\_gateway\_mode | `""` \| `"custom"` \| `"off"` | no | |
| ai\_gateway\_token | `string` | no | |
| anthropic\_api\_key | `string` | no | |
| anthropic\_base\_url | `string` | no | |
| anthropic\_credential\_source | `""` \| `"api_key"` \| `"cloudflare"` | no | |
| anthropic\_enabled | `""` \| `"true"` \| `"false"` | no | |
| openai\_api\_key | `string` | no | |
| openai\_base\_url | `string` | no | |
| openai\_credential\_source | `""` \| `"api_key"` \| `"cloudflare"` | no | |
| openai\_enabled | `""` \| `"true"` \| `"false"` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `AIProviderSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/settings/ai/routes`
[Section titled “GET /api/orgs/{org\_id}/settings/ai/routes”](#get-apiorgsorg_idsettingsairoutes)
Get Route Settings
Every inference route with this org’s per-route credentials (secrets masked).
One row per route the platform defines, including routes the org has never configured and routes the platform has disabled — an org cannot invent a route, so the platform’s list is the whole list, and an omitted row would read as “no such route” rather than “not configured”.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `OrgRouteSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/settings/ai/routes`
[Section titled “PATCH /api/orgs/{org\_id}/settings/ai/routes”](#patch-apiorgsorg_idsettingsairoutes)
Update Route Settings
Set this org’s credentials, endpoint override and toggle for one or more routes.
Only routes present in the body are touched, and within a route only the fields present. An explicit `null` clears a field; clearing is allowed on every plan, so a downgrade never strands a value the org can no longer remove.
Two refusals are specific to this endpoint, and both would otherwise store a value nothing reads:
* **A slug that is not a live route** answers 422 naming it, and nothing at all is persisted. It is not silently dropped — the org settings store skips keys it does not recognise and returns 200, which for a mistyped route would mean an admin watching a key “save” that was never stored. An unreadable route table produces an empty list and therefore refuses every write, which is the safe direction: refusing a legitimate save is visible, accepting an illegitimate one is not.
* **A route that spends another route’s key** (`credential_route` set) refuses `api_key` and `credential_source`. `anthropic-cf` sends `anthropic-direct`’s credential by design; a key stored on the `-cf` row would be silently unread, and an admin would reasonably conclude the gateway was configured.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------ | -------- | -------- | ----------- |
| routes | `object` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `OrgRouteSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/settings/limits`
[Section titled “GET /api/orgs/{org\_id}/settings/limits”](#get-apiorgsorg_idsettingslimits)
Get Org Limits
Get model governance (two allowlists + defaults) and budget for the org.
Reads every stored value back unfiltered, including one the org’s current plan would now refuse on `PATCH` and one the runtime no longer enforces. That is deliberate and load-bearing: a value that still exists on the account has to stay *visible* to the admin who set it, or gating the write just recreates the invisible-setting problem the gate exists to remove — this time with the value hidden instead of merely unexplained. Enforcement and display are separate questions.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgLimitsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/settings/limits`
[Section titled “PATCH /api/orgs/{org\_id}/settings/limits”](#patch-apiorgsorg_idsettingslimits)
Update Org Limits
Update the org’s two model allowlists, default models, and/or budget.
Only fields present in the request body are updated; omitted fields are left unchanged. For default-model fields, an explicit `null` clears the default.
Invariant: when a scope’s allowlist is non-empty, its default model must be set and within the allowlist (422 otherwise). Response `affected_agents` lists agents left outside the (new) agent allowlist — they downgrade to the agent default at run time.
Two groups of fields here are plan-gated and answer 403 when set (never when cleared): the model controls need `direct_model_choice`, and `budget_limit_usd` is refused to an org whose plan `enforces_credits`. See :data:`MODEL_CHOICE_LIMIT_KEYS` and the guard below.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------------------- | ------------------------ | -------- | ----------- |
| agent\_default\_model | `DefaultModel` | no | |
| agent\_model\_allowlist | `string[]` | no | |
| budget\_limit\_usd | `string` | no | |
| chat\_default\_model | `DefaultModel` | no | |
| chat\_model\_allowlist | `string[]` | no | |
| intake\_classifier\_default\_model | `DefaultModel` | no | |
| process\_default\_model | `DefaultModel` | no | |
| prompt\_model\_allowlist | `string[]` | no | |
| tool\_router\_default\_model | `ToolRouterDefaultModel` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgLimitsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/settings/limits/usage`
[Section titled “GET /api/orgs/{org\_id}/settings/limits/usage”](#get-apiorgsorg_idsettingslimitsusage)
Get Org Limits Usage
Get the org’s current spend vs. its configured budget cap.
Returns two spend figures (M11 — see `OrgLimitsUsageResponse`): the chat-attributed total the budget cap enforces against (`spend_usd`) and the fuller metered total including chat-less LLM calls (`metered_spend_usd`), so the two no longer read as unlabelled disagreeing numbers. Useful for the admin limits panel.
`budget_limit_usd` is reported as stored even for an org whose plan `enforces_credits` and for which the runner therefore no longer reads it — same reason as `GET /settings/limits`: a stored value stays visible to whoever set it.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `OrgLimitsUsageResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/settings/runtime`
[Section titled “GET /api/orgs/{org\_id}/settings/runtime”](#get-apiorgsorg_idsettingsruntime)
Get Runtime Settings
Get the org’s AI runtime overrides (compaction + model thinking).
A null field means the org inherits; `platform_defaults` says what that inheritance currently resolves to.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `OrgRuntimeSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/settings/runtime`
[Section titled “PATCH /api/orgs/{org\_id}/settings/runtime”](#patch-apiorgsorg_idsettingsruntime)
Update Runtime Settings
Set (or clear) the org’s AI runtime overrides.
Omitted fields are left unchanged; an explicit `null` drops the override so the setting falls back to the platform tier. Values are re-read at the start of every run, so a change here lands on the next turn.
`thinking_enabled` needs `direct_model_choice`: on a plan that sells effort *modes*, the preset carries thinking, effort and model together, and the runtime half drops any stored value (:data:`~agentdepot_core.services.model_policy.MODE_OWNED_RUNTIME_KEYS`).
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------------------- | --------- | -------- | ----------- |
| compaction\_enabled | `boolean` | no | |
| compaction\_threshold | `number` | no | |
| decision\_log\_enabled | `boolean` | no | |
| thinking\_enabled | `boolean` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `OrgRuntimeSettingsResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Org Variables
> REST API reference for org variables.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/variables`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/variables`
[Section titled “GET /api/orgs/{org\_id}/variables”](#get-apiorgsorg_idvariables)
List Variables
List all user-defined variables for the organization (secrets redacted).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `OrgVariableResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/variables`
[Section titled “POST /api/orgs/{org\_id}/variables”](#post-apiorgsorg_idvariables)
Create Variable
Create a new user-defined variable.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | --------- | -------- | ----------- |
| description | `string` | no | |
| is\_secret | `boolean` | no | |
| key | `string` | yes | |
| value | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `OrgVariableResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/variables/{variable_id}`
[Section titled “PUT /api/orgs/{org\_id}/variables/{variable\_id}”](#put-apiorgsorg_idvariablesvariable_id)
Update Variable
Update the value of a user-defined variable.
System variables cannot be updated. They are where the plan-gated settings live (`budget_limit_usd`, the model allowlists, the provider toggles, `thinking_enabled`), each of which is guarded on the endpoint that owns it — so writing one through here would route around that guard.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| variable\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | no | |
| value | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgVariableResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/variables/{variable_id}`
[Section titled “DELETE /api/orgs/{org\_id}/variables/{variable\_id}”](#delete-apiorgsorg_idvariablesvariable_id)
Delete Variable
Delete a user-defined variable. System variables cannot be deleted.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| variable\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# Organizations
> REST API reference for organizations.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs`
[Section titled “GET /api/orgs”](#get-apiorgs)
List Orgs
List all organizations the authenticated user belongs to.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs`
[Section titled “POST /api/orgs”](#post-apiorgs)
Create Org
Create a new organization.
The authenticated user becomes the OWNER of the new organization. With no `slug` the service derives and uniquifies one from the name; an explicit slug is validated (shape, length, reserved words) and must be free.
Refused with 403 when new orgs land on `trial` and the caller is already at `credits_max_trial_orgs_per_user`. Platform staff are exempt: creating throwaway orgs is how the credits and metering sweeps get a clean room, and that is a support/testing action rather than the signup path the cap guards.
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | yes | |
| slug | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `OrgResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}`
[Section titled “GET /api/orgs/{org\_id}”](#get-apiorgsorg_id)
Get Org
Get organization details.
Requires membership in the organization (user) or belonging to it (agent).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}`
[Section titled “PATCH /api/orgs/{org\_id}”](#patch-apiorgsorg_id)
Update Org
Update organization details.
Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | no | |
| slug | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OrgResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}`
[Section titled “DELETE /api/orgs/{org\_id}”](#delete-apiorgsorg_id)
Delete Org
Delete an organization.
Requires OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/slug-rules`
[Section titled “GET /api/orgs/slug-rules”](#get-apiorgsslug-rules)
Get Slug Rules
The rules an organization address must satisfy.
Served so the create-organization wizard can flag a reserved or malformed address as the user types, without a hand-copied duplicate of the reserved list rotting in the frontend. The server-side check in `create_org` is still the authority.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SlugRulesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Overview
> REST API reference for overview.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/overview`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/overview`
[Section titled “GET /api/orgs/{org\_id}/overview”](#get-apiorgsorg_idoverview)
Get Overview
Aggregate dashboard overview for an org.
`since` scopes the work/cost stats and errored-chat count; inventory, recent activity, and the paused-chat breakdown are current-state and window-independent.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------- |
| org\_id | path | `string (uuid)` | yes | |
| since | query | `string` | no | Stats window: 24h, 7d, 30d (default 30d) |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `OverviewResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/overview/recent-chats`
[Section titled “GET /api/orgs/{org\_id}/overview/recent-chats”](#get-apiorgsorg_idoverviewrecent-chats)
List Recent Chats
Paginated recent chats, for infinite-scrolling sidebar lists.
Unlike `recent_chats` on the main overview payload (fixed, capped at 20, attention chats surfaced first), this excludes attention-requiring chats entirely — those are handled by the top-bar “Needs attention” dropdown — and supports paging through the rest.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| page | query | `integer` | no | |
| per\_page | query | `integer` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `RecentChatsPageResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Processes
> REST API reference for processes.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/processes`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/processes`
[Section titled “GET /api/orgs/{org\_id}/processes”](#get-apiorgsorg_idprocesses)
List Processes
List processes for the organization.
Omit `limit` to get every process; `total` is the full count either way.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ------------------------------------------------------------ |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | query | `string` | no | Filter by project id, or ‘none’ for uncategorized processes. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `Page_ProcessResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/processes`
[Section titled “POST /api/orgs/{org\_id}/processes”](#post-apiorgsorg_idprocesses)
Create Process
Create a process (no revision yet). Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | yes | |
| title | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `ProcessResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}”](#get-apiorgsorg_idprocessesprocess_id)
Get Process
Get a process, including its current deployed revision (if any).
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ProcessResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/processes/{process_id}`
[Section titled “PATCH /api/orgs/{org\_id}/processes/{process\_id}”](#patch-apiorgsorg_idprocessesprocess_id)
Update Process
Update a process’s title/description. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | yes | |
| title | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ProcessResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/processes/{process_id}`
[Section titled “DELETE /api/orgs/{org\_id}/processes/{process\_id}”](#delete-apiorgsorg_idprocessesprocess_id)
Delete Process
Soft-delete a process. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/revisions`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/revisions”](#get-apiorgsorg_idprocessesprocess_idrevisions)
List Revisions
List a process’s revisions, newest version first.
Not itself in the Phase-1 milestone’s endpoint list, but the CLI’s `deploy --revision N` (and “deploy latest”) need a way to resolve a version to a revision id, so it’s added here as the minimal completion.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `ProcessRevisionResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/processes/{process_id}/revisions`
[Section titled “POST /api/orgs/{org\_id}/processes/{process\_id}/revisions”](#post-apiorgsorg_idprocessesprocess_idrevisions)
Push Revision
Push a draft revision from a definition. Requires admin/owner role.
Parse errors return 422 carrying the validator’s plain wording (step id, field, what is wrong, what is valid) rather than the raw pydantic dump. Pushing a definition whose hash matches the latest revision is a no-op — the existing revision is returned (200) instead of creating a duplicate.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ---------- | -------- | -------- | ----------- |
| definition | `object` | yes | |
| sop\_text | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 201 | Successful Response | `ProcessRevisionResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/processes/{process_id}/revisions/{revision_id}/deploy`
[Section titled “POST /api/orgs/{org\_id}/processes/{process\_id}/revisions/{revision\_id}/deploy”](#post-apiorgsorg_idprocessesprocess_idrevisionsrevision_iddeploy)
Deploy Revision
Validate + deploy a draft revision. Requires admin/owner role.
Validation errors return 422 with `{errors, warnings}` in the validator’s plain-language wording, verbatim. On success the revision’s tool snapshot is pinned, the previously-deployed revision is archived, and the process’s `stale_reason` is cleared (a re-deploy is the re-evaluation).
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| revision\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `DeployResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs”](#get-apiorgsorg_idprocessesprocess_idruns)
List Runs
List runs for a process, newest first. Optional `status` filter.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| status | query | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `ProcessRunListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}”](#get-apiorgsorg_idprocessesprocess_idrunsrun_id)
Get Run
Get a run’s detail, including its steps ordered by creation time.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `ProcessRunDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/agent-calls`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/agent-calls”](#get-apiorgsorg_idprocessesprocess_idrunsrun_idagent-calls)
List Run Agent Calls
Scalar-only index of a run’s `agent` block step turns/distill calls.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `ProcessAgentCallListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/agent-calls/{call_id}`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/agent-calls/{call\_id}”](#get-apiorgsorg_idprocessesprocess_idrunsrun_idagent-callscall_id)
Get Run Agent Call
Full detail (incl. request/response messages) for one agent-step call.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| call\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------------- |
| 200 | Successful Response | `ProcessAgentCallDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/cancel`
[Section titled “POST /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/cancel”](#post-apiorgsorg_idprocessesprocess_idrunsrun_idcancel)
Cancel Run
Cancel a running / human-waiting run.
Tombstones the run row (step children check it at every step boundary) and cancels the durable Hatchet parent plus any in-flight step children. A run already in a terminal state is a no-op (`cancelled: false`).
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `CancelRunResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/events`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/events”](#get-apiorgsorg_idprocessesprocess_idrunsrun_idevents)
List Run Events
A run’s timeline, ordered oldest first.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `ProcessRunEventListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/llm-calls`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/llm-calls”](#get-apiorgsorg_idprocessesprocess_idrunsrun_idllm-calls)
List Run Llm Calls
Scalar-only index of a run’s `llm`/`prompt` block exchanges.
The heavy `request_messages`/`response_message` JSONB is never loaded — use `GET .../llm-calls/{call_id}` for the full exchange.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------------- |
| 200 | Successful Response | `ProcessLlmCallListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/llm-calls/{call_id}`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/llm-calls/{call\_id}”](#get-apiorgsorg_idprocessesprocess_idrunsrun_idllm-callscall_id)
Get Run Llm Call
Full detail (incl. request/response messages) for one LLM call.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| call\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `ProcessLlmCallDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/repairs`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/repairs”](#get-apiorgsorg_idprocessesprocess_idrunsrun_idrepairs)
List Run Repairs
Index of a run’s tool-payload repair verdicts.
The scalar/text fields (`tool_slug`/`error`/`fixable`/`reason`/ `payload_diff`) ARE the payload here; the heavy raw exchange is still excluded — use `GET .../repairs/{call_id}` for that.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------- |
| 200 | Successful Response | `ProcessRepairCallListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/repairs/{call_id}`
[Section titled “GET /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/repairs/{call\_id}”](#get-apiorgsorg_idprocessesprocess_idrunsrun_idrepairscall_id)
Get Run Repair
Full detail (incl. request/response messages) for one repair call.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| call\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------------- |
| 200 | Successful Response | `ProcessRepairCallDetailResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/processes/{process_id}/runs/{run_id}/restart`
[Section titled “POST /api/orgs/{org\_id}/processes/{process\_id}/runs/{run\_id}/restart”](#post-apiorgsorg_idprocessesprocess_idrunsrun_idrestart)
Restart Run
Restart a terminal run as a brand-new run on the process’s current deployed revision.
Only terminal runs (`completed` / `flagged` / `failed` / `cancelled`) can be restarted — an active run (`running` / `waiting_human`) must be cancelled first. The new run replays the old run’s envelope against whatever revision is *currently* deployed (by design — restart always runs the current revision, not a pinned replay) and does not touch the original run.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| process\_id | path | `string` | yes | |
| run\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `ProcessRunResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Projects
> REST API reference for projects.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/projects`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/projects`
[Section titled “GET /api/orgs/{org\_id}/projects”](#get-apiorgsorg_idprojects)
List Projects
List all projects in the organization, with resource counts by type.
Omit `limit` to get every project; `total` is the full count either way. `uncategorized_count` is always the whole-org count, independent of the page being viewed.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ProjectListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/projects`
[Section titled “POST /api/orgs/{org\_id}/projects”](#post-apiorgsorg_idprojects)
Create Project
Create a new project. Requires admin/owner role.
The three `shared_*` fields form the project’s shared context. Both lists are resolved against the org before they are stored — an unknown tool scope or skill slug is a 422.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| color | `string` | no | |
| description | `string` | no | |
| icon | `string` | no | |
| name | `string` | yes | |
| shared\_allowed\_tools | `string[]` | no | |
| shared\_instruction | `string` | no | |
| shared\_mounted\_skills | `string[]` | no | |
| slug | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `ProjectResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/projects/{project_id}`
[Section titled “GET /api/orgs/{org\_id}/projects/{project\_id}”](#get-apiorgsorg_idprojectsproject_id)
Get Project
Get a project’s detail, including resource counts by type.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ProjectResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/projects/{project_id}`
[Section titled “PATCH /api/orgs/{org\_id}/projects/{project\_id}”](#patch-apiorgsorg_idprojectsproject_id)
Update Project
Update a project’s fields, including its shared context. Requires admin/owner role.
Only fields present in the request body are applied; pass `null` to clear `description`, `color`, `icon`, or `shared_instruction`. The two shared lists REPLACE the stored list — send `[]` to clear them, omit them to leave them alone. Both are resolved against the org before storing (422 on an unknown tool scope or skill slug).
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------------- | ---------- | -------- | ----------- |
| color | `string` | no | |
| description | `string` | no | |
| icon | `string` | no | |
| name | `string` | no | |
| shared\_allowed\_tools | `string[]` | no | |
| shared\_instruction | `string` | no | |
| shared\_mounted\_skills | `string[]` | no | |
| slug | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ProjectResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/projects/{project_id}`
[Section titled “DELETE /api/orgs/{org\_id}/projects/{project\_id}”](#delete-apiorgsorg_idprojectsproject_id)
Delete Project
Soft-delete a project. Requires admin/owner role.
Membership rows are hard-deleted; bundled resources are NOT deleted — they become uncategorized.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/projects/{project_id}/memories`
[Section titled “GET /api/orgs/{org\_id}/projects/{project\_id}/memories”](#get-apiorgsorg_idprojectsproject_idmemories)
List Project Memories
List memories owned by this project, most recent first. Requires org membership.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------- |
| 200 | Successful Response | `ProjectMemoryListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/projects/{project_id}/memories`
[Section titled “POST /api/orgs/{org\_id}/projects/{project\_id}/memories”](#post-apiorgsorg_idprojectsproject_idmemories)
Create Project Memory
Create a memory owned by this project. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------- | -------- | -------- | ----------- |
| content | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `AgentMemoryResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/projects/{project_id}/memories/{memory_id}`
[Section titled “DELETE /api/orgs/{org\_id}/projects/{project\_id}/memories/{memory\_id}”](#delete-apiorgsorg_idprojectsproject_idmemoriesmemory_id)
Delete Project Memory
Delete a project memory. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| memory\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/projects/{project_id}/resources`
[Section titled “GET /api/orgs/{org\_id}/projects/{project\_id}/resources”](#get-apiorgsorg_idprojectsproject_idresources)
List Project Resources
List a project’s member resources, resolved to display data.
Dangling rows (the referenced entity was deleted) are skipped gracefully.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `ProjectResourcesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/projects/{project_id}/resources`
[Section titled “POST /api/orgs/{org\_id}/projects/{project\_id}/resources”](#post-apiorgsorg_idprojectsproject_idresources)
Add Project Resources
Batch-add resources to a project. Requires admin/owner role.
A resource already in another project is moved (a resource belongs to at most one project). Validates every entity exists in this org; 422 if any don’t (this also rejects system skills, since `org_id IS NULL` skills never match an org-scoped lookup).
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------- | --------------- | -------- | ----------- |
| resources | `ResourceRef[]` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `AddResourcesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/projects/{project_id}/resources/{entity_type}/{entity_id}`
[Section titled “DELETE /api/orgs/{org\_id}/projects/{project\_id}/resources/{entity\_type}/{entity\_id}”](#delete-apiorgsorg_idprojectsproject_idresourcesentity_typeentity_id)
Remove Project Resource
Remove one resource from a project. Requires admin/owner role.
The resource is NOT deleted — it becomes uncategorized.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------------- | -------- | ----------- |
| entity\_id | path | `string (uuid)` | yes | |
| entity\_type | path | `ProjectResourceType` | yes | |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/projects/reorder`
[Section titled “PUT /api/orgs/{org\_id}/projects/reorder”](#put-apiorgsorg_idprojectsreorder)
Reorder Projects
Reorder projects by setting `display_order`. Requires membership.
Ordering is a per-org display preference, not a configuration change, so this needs membership rather than admin/owner — same as `PUT /agent-teams/reorder`. Ids that are not live projects of this org are skipped; the rest still reorder.
Registered ahead of `/{project_id}` so the literal segment is matched first even if a verb is later added to that path.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------ | ----------------- | -------- | ----------- |
| project\_ids | `string (uuid)[]` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# Prompt Templates
> REST API reference for prompt templates.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/prompt-templates`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/prompt-templates`
[Section titled “GET /api/orgs/{org\_id}/prompt-templates”](#get-apiorgsorg_idprompt-templates)
List Prompt Templates
List prompt templates for the organization.
Omit `limit` to get every template; `total` is the full count either way.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ------------------------------------------------------------ |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | query | `string` | no | Filter by project id, or ‘none’ for uncategorized templates. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------ |
| 200 | Successful Response | `Page_PromptTemplateResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/prompt-templates`
[Section titled “POST /api/orgs/{org\_id}/prompt-templates”](#post-apiorgsorg_idprompt-templates)
Create Prompt Template
Create a new prompt template. Requires admin/owner role.
`fields` (optional) defines an extraction schema — when present, the template runs as structured extraction instead of free-text generation.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------------------------ | --------------------------------------------- | -------- | ----------- |
| description | `string` | no | |
| fields | `PromptFieldSchema[]` | no | |
| file\_input\_mode | `"auto"` \| `"image"` \| `"text"` \| `"both"` | no | |
| model | `string` | no | |
| name | `string` | yes | |
| post\_processing\_instructions | `string` | no | |
| reasoning\_effort | `string` | no | |
| tags | `string[]` | no | |
| text | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 201 | Successful Response | `PromptTemplateResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/prompt-templates/{id_or_slug}`
[Section titled “GET /api/orgs/{org\_id}/prompt-templates/{id\_or\_slug}”](#get-apiorgsorg_idprompt-templatesid_or_slug)
Get Prompt Template
Get a prompt template by ID or slug.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `PromptTemplateResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/prompt-templates/{id_or_slug}/test`
[Section titled “POST /api/orgs/{org\_id}/prompt-templates/{id\_or\_slug}/test”](#post-apiorgsorg_idprompt-templatesid_or_slugtest)
Test Prompt Template
Stream a test execution of a prompt template. Requires admin/owner role.
Drives BOTH modes via the unified `stream_prompt_test` runner: a plain prompt (no `fields` on the active revision) streams free-text `token`/`done` events; a schema-bearing prompt streams the structured `extracting`/`result`/`post_processing`/`done` events and requires material input via `input_text` or `input_url`.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| attachments | `PromptTestAttachment[]` | no | |
| file\_input\_mode | `"auto"` \| `"image"` \| `"text"` \| `"both"` | no | |
| input\_text | `string` | no | |
| input\_url | `string` | no | |
| model | `string` | no | |
| model\_mode | `string` | no | Effort level to test with, for an org whose plan selects effort rather than models: ‘trivial’ (Trivial), ‘normal’ (Standard), ‘high\_effort’ (High) or ‘x\_high’ (X-High). The platform decides what each level runs (model + reasoning effort + thinking) — see GET /model-modes. No level is plan-gated. Rejected with 422 for an org whose plan has direct model choice, where a level would have no effect — such an org sets `model` instead. Omit to run on the default level (Standard). |
| reasoning\_effort | `string` | no | |
| variables | `object` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `object` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/prompt-templates/{template_id}`
[Section titled “PUT /api/orgs/{org\_id}/prompt-templates/{template\_id}”](#put-apiorgsorg_idprompt-templatestemplate_id)
Update Prompt Template
Update a prompt template by ID or slug. Requires admin/owner role.
Passing `text`, `fields`, or `post_processing_instructions` creates a new revision (versioned together as one snapshot).
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| template\_id | path | `string` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------------------------ | --------------------------------------------- | -------- | ----------- |
| description | `string` | yes | |
| fields | `PromptFieldSchema[]` | no | |
| file\_input\_mode | `"auto"` \| `"image"` \| `"text"` \| `"both"` | yes | |
| model | `string` | yes | |
| name | `string` | no | |
| post\_processing\_instructions | `string` | no | |
| reasoning\_effort | `string` | yes | |
| tags | `string[]` | no | |
| text | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------ |
| 200 | Successful Response | `PromptTemplateResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/prompt-templates/{template_id}`
[Section titled “DELETE /api/orgs/{org\_id}/prompt-templates/{template\_id}”](#delete-apiorgsorg_idprompt-templatestemplate_id)
Delete Prompt Template
Soft-delete a prompt template by ID or slug. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| template\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/prompt-templates/{template_id}/revisions`
[Section titled “GET /api/orgs/{org\_id}/prompt-templates/{template\_id}/revisions”](#get-apiorgsorg_idprompt-templatestemplate_idrevisions)
List Prompt Template Revisions
List all revisions for a prompt template (by ID or slug).
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| template\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------------- |
| 200 | Successful Response | `PromptTemplateRevisionSummary[]` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/prompt-templates/{template_id}/runs`
[Section titled “GET /api/orgs/{org\_id}/prompt-templates/{template\_id}/runs”](#get-apiorgsorg_idprompt-templatestemplate_idruns)
List Prompt Template Runs
List runs for a prompt template (by ID or slug).
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| offset | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| template\_id | path | `string` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------------- |
| 200 | Successful Response | `PromptTemplateRunResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
# Resources
> REST API reference for resources.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/resources`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/resources`
[Section titled “GET /api/orgs/{org\_id}/resources”](#get-apiorgsorg_idresources)
List Available Resources
List resources available to add to bays.
Discovers resources from connected providers, filtered by capability.
**Parameters**
| Name | In | Type | Required | Description |
| ---------- | ----- | --------------- | -------- | ---------------------------------------------- |
| capability | query | `string` | yes | Filter by resource capability (e.g. ‘context’) |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------------- |
| 200 | Successful Response | `AvailableResourcesListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Schedules
> REST API reference for schedules.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/agents/{agent_id}/schedules`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/agents/{agent_id}/schedules`
[Section titled “GET /api/orgs/{org\_id}/agents/{agent\_id}/schedules”](#get-apiorgsorg_idagentsagent_idschedules)
List Agent Schedules
List all schedules for one agent, oldest first.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ScheduleResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/agents/{agent_id}/schedules`
[Section titled “POST /api/orgs/{org\_id}/agents/{agent\_id}/schedules”](#post-apiorgsorg_idagentsagent_idschedules)
Create Schedule
Create a recurring cron schedule that fires this agent.
Requires ADMIN or OWNER role. `cron_expr` (standard 5-field) and `timezone` (IANA) are validated together; an invalid combination returns 422.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| agent\_id | path | `string (uuid)` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------------- | ----------------------- | -------- | ----------- |
| cron\_expr | `string` | yes | |
| enabled | `boolean` | no | |
| overlap\_policy | `ScheduleOverlapPolicy` | no | |
| prompt | `string` | yes | |
| timezone | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `ScheduleResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/schedules`
[Section titled “GET /api/orgs/{org\_id}/schedules”](#get-apiorgsorg_idschedules)
List Org Schedules
List schedules in the org, each annotated with its agent’s name/slug.
Omit `limit` to get every schedule; `total` is the full count either way.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------------- |
| 200 | Successful Response | `Page_ScheduleWithAgentResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/schedules/{schedule_id}`
[Section titled “GET /api/orgs/{org\_id}/schedules/{schedule\_id}”](#get-apiorgsorg_idschedulesschedule_id)
Get Schedule
Get a single schedule’s detail.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| schedule\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ScheduleResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/schedules/{schedule_id}`
[Section titled “PATCH /api/orgs/{org\_id}/schedules/{schedule\_id}”](#patch-apiorgsorg_idschedulesschedule_id)
Update Schedule
Update any of prompt/cron\_expr/timezone/enabled/overlap\_policy.
Requires ADMIN or OWNER role. Recomputes `next_run_at` when `cron_expr`/`timezone`/`enabled` change; an invalid cron/timezone combination returns 422.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| schedule\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| --------------- | ----------------------- | -------- | ----------- |
| cron\_expr | `string` | no | |
| enabled | `boolean` | no | |
| overlap\_policy | `ScheduleOverlapPolicy` | no | |
| prompt | `string` | no | |
| timezone | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ScheduleResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/schedules/{schedule_id}`
[Section titled “DELETE /api/orgs/{org\_id}/schedules/{schedule\_id}”](#delete-apiorgsorg_idschedulesschedule_id)
Delete Schedule
Soft-delete a schedule. Requires ADMIN or OWNER role.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| schedule\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/schedules/{schedule_id}/runs`
[Section titled “GET /api/orgs/{org\_id}/schedules/{schedule\_id}/runs”](#get-apiorgsorg_idschedulesschedule_idruns)
List Schedule Runs
List the chats this schedule has fired (the correlation query), newest first.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| schedule\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `ScheduleRunResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
# Search
> REST API reference for search.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/search`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/search`
[Section titled “GET /api/orgs/{org\_id}/search”](#get-apiorgsorg_idsearch)
Global Search
Full-text search across chats and agents within an org.
`status` (chat lifecycle) and `agent_id` only apply to chat results; an agent match by name is returned regardless of chat status, and `agent_id` (searching within one already-known agent’s chats) suppresses agent-name results rather than filtering them.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------------------------- |
| agent\_id | query | `string (uuid)` | no | Filter results by agent |
| limit | query | `integer` | no | Maximum results to return |
| offset | query | `integer` | no | Pagination offset |
| org\_id | path | `string (uuid)` | yes | |
| q | query | `string` | yes | Search query text |
| status | query | `ChatStatus[]` | no | Filter results by chat status |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------------------------------- |
| 200 | Successful Response | `agentdepot_api__routers__search__SearchResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Skills
> REST API reference for skills.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/skills`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/skills`
[Section titled “GET /api/orgs/{org\_id}/skills”](#get-apiorgsorg_idskills)
List Skills
List skills in the organization.
Omit `limit` to get every skill; `total` is the full count either way.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
| project\_id | query | `string` | no | Filter by project id, or ‘none’ for uncategorized skills. |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_SkillResponse_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/skills`
[Section titled “POST /api/orgs/{org\_id}/skills”](#post-apiorgsorg_idskills)
Create Skill
Create a new skill. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------------- | ---------- | -------- | ----------- |
| description | `string` | no | |
| instructions | `string` | yes | |
| name | `string` | yes | |
| reference\_material | `string` | no | |
| slug | `string` | no | |
| tool\_slugs | `string[]` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `SkillResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/skills/{id_or_slug}`
[Section titled “GET /api/orgs/{org\_id}/skills/{id\_or\_slug}”](#get-apiorgsorg_idskillsid_or_slug)
Get Skill
Get a skill by ID or slug, including active and draft revisions.
**Parameters**
| Name | In | Type | Required | Description |
| ------------ | ---- | --------------- | -------- | ----------- |
| id\_or\_slug | path | `string` | yes | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SkillResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### PUT `/api/orgs/{org_id}/skills/{skill_id}`
[Section titled “PUT /api/orgs/{org\_id}/skills/{skill\_id}”](#put-apiorgsorg_idskillsskill_id)
Update Skill
Update a skill. name/description update the parent; instructions/tool\_slugs/reference\_material go to a draft revision. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| skill\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ------------------- | ---------- | -------- | ----------- |
| description | `string` | no | |
| instructions | `string` | no | |
| name | `string` | no | |
| reference\_material | `string` | no | |
| tool\_slugs | `string[]` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SkillResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/skills/{skill_id}`
[Section titled “DELETE /api/orgs/{org\_id}/skills/{skill\_id}”](#delete-apiorgsorg_idskillsskill_id)
Delete Skill
Soft-delete a skill. Requires admin/owner role.
Also unmounts the skill everywhere it was mounted — its slug is removed from every agent’s `mounted_skills` and every team’s `shared_mounted_skills`, so nothing keeps reporting a skill that no longer exists. Chats already running keep the copy frozen into their snapshot.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| skill\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/skills/{skill_id}/deploy`
[Section titled “POST /api/orgs/{org\_id}/skills/{skill\_id}/deploy”](#post-apiorgsorg_idskillsskill_iddeploy)
Deploy Skill
Promote the draft revision to active. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| skill\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SkillResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/skills/{skill_id}/draft`
[Section titled “DELETE /api/orgs/{org\_id}/skills/{skill\_id}/draft”](#delete-apiorgsorg_idskillsskill_iddraft)
Discard Skill Draft
Discard the draft revision for a skill. Requires admin/owner role.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| skill\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SkillResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/skills/{skill_id}/revisions`
[Section titled “GET /api/orgs/{org\_id}/skills/{skill\_id}/revisions”](#get-apiorgsorg_idskillsskill_idrevisions)
List Skill Revisions
List all revisions for a skill, newest first.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| skill\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `SkillRevisionResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/skills/{skill_id}/usage`
[Section titled “GET /api/orgs/{org\_id}/skills/{skill\_id}/usage”](#get-apiorgsorg_idskillsskill_idusage)
Get Skill Usage
List activation records for a skill, newest first.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------- |
| limit | query | `integer` | no | |
| org\_id | path | `string (uuid)` | yes | |
| skill\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------------------- |
| 200 | Successful Response | `SkillActivationRecordResponse[]` |
| 422 | Validation Error | `HTTPValidationError` |
# Spend
> REST API reference for spend.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/spend/agents/{agent_id}`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/spend/agents/{agent_id}`
[Section titled “GET /api/orgs/{org\_id}/spend/agents/{agent\_id}”](#get-apiorgsorg_idspendagentsagent_id)
Get Agent Spend
One agent’s spend, plus the tool calls and metered activity behind it.
The drill-down from the overview’s agent table. Most tools carry no price of their own — the cost of a tool call is the LLM turn it forces, not the call — so the tool table leads with call and error counts rather than dollars.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | -------------------------------- |
| agent\_id | path | `string (uuid)` | yes | |
| days | query | `integer` | no | Window size in days, ending now. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `AgentSpendDetail` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/spend/overview`
[Section titled “GET /api/orgs/{org\_id}/spend/overview”](#get-apiorgsorg_idspendoverview)
Get Spend Overview
What the org spent in the window, grouped by agent, model and chat.
Each grouping is capped at a top-N by spend; `truncated` says when that bit, so a panel can say so rather than implying these were all the spenders there were.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | -------------------------------- |
| days | query | `integer` | no | Window size in days, ending now. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `SpendOverview` |
| 422 | Validation Error | `HTTPValidationError` |
# Tags
> REST API reference for tags.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/tags`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/tags`
[Section titled “GET /api/orgs/{org\_id}/tags”](#get-apiorgsorg_idtags)
List Tags
List all tags in the org registry, ordered by name.
Omit `limit` to get every tag; `total` is the full count either way.
**Auth**: org member.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------------------------------- |
| limit | query | `integer` | no | Max rows to return (1-100). Omit to return every row. |
| offset | query | `integer` | no | Rows to skip — pass the previous response’s `next_offset`. |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `Page_TagRead_` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/tags`
[Section titled “POST /api/orgs/{org\_id}/tags”](#post-apiorgsorg_idtags)
Create Tag
Create a new tag in the org registry.
The name is normalized (lowercased, trimmed) before storage. If a tag with the resulting name already exists, the existing tag is returned (idempotent).
**Auth**: org admin/owner.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | --------------------------------------------------- |
| color | `string` | no | Optional color hex/name. |
| description | `string` | no | Optional human-readable description. |
| name | `string` | yes | Tag name. Will be normalized (lowercased, trimmed). |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `TagRead` |
| 422 | Validation Error | `HTTPValidationError` |
### PATCH `/api/orgs/{org_id}/tags/{tag_id}`
[Section titled “PATCH /api/orgs/{org\_id}/tags/{tag\_id}”](#patch-apiorgsorg_idtagstag_id)
Update Tag
Update a tag’s name, color, or description.
Only fields present in the request body are applied. Pass `null` to clear `color` or `description`.
A rename is propagated into chats’ tag lists — see `ChatService.rename_tag`: chats store tags as a plain string array rather than through this registry, so a rename here would otherwise leave chats showing the old name.
**Auth**: org admin/owner.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| tag\_id | path | `string (uuid)` | yes | |
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| color | `string` | no | |
| description | `string` | no | |
| name | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `TagRead` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/orgs/{org_id}/tags/{tag_id}`
[Section titled “DELETE /api/orgs/{org\_id}/tags/{tag\_id}”](#delete-apiorgsorg_idtagstag_id)
Delete Tag
Soft-delete a tag from the registry.
Also removes all entity-tag associations for this tag, and strips its name from any chat’s tag list (chats aren’t on the entity-tag registry — see `ChatService.remove_tag`) so no chat is left pointing at a deleted tag.
**Auth**: org admin/owner.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
| tag\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# API Tokens
> REST API reference for api tokens.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/tokens`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/tokens`
[Section titled “GET /api/tokens”](#get-apitokens)
List Tokens
List all API tokens for the current user.
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `TokenListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/tokens`
[Section titled “POST /api/tokens”](#post-apitokens)
Create Token
Create a new API token.
The plain token is only returned once in this response. Store it securely - it cannot be retrieved again.
**Request body** (required)
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| description | `string` | no | |
| name | `string` | yes | |
| scope | `string` | no | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 201 | Successful Response | `CreateTokenResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### DELETE `/api/tokens/{token_id}`
[Section titled “DELETE /api/tokens/{token\_id}”](#delete-apitokenstoken_id)
Revoke Token
Revoke an API token.
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ---- | --------------- | -------- | ----------- |
| token\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 204 | Successful Response | |
| 422 | Validation Error | `HTTPValidationError` |
# Tool Sources
> REST API reference for tool sources.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/tool-sources`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/tool-sources`
[Section titled “GET /api/orgs/{org\_id}/tool-sources”](#get-apiorgsorg_idtool-sources)
List Tool Sources
List available tool sources for an org.
Returns built-in AgentDepot/system sources and any upstream MCP servers the caller can use: org-shared ones plus their own private connections (:func:`_viewer_user_id`). `IntegrationService.list_integrations` applies no visibility filter of its own, so without the check below this endpoint listed *other members’* private connections as tool sources — the mirror image of the tools listing, which used to hide the caller’s own. Requires org membership.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ToolSourcesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/tool-sources/decision-coverage`
[Section titled “GET /api/orgs/{org\_id}/tool-sources/decision-coverage”](#get-apiorgsorg_idtool-sourcesdecision-coverage)
Decision Coverage
What each decision mode would cover, per connection.
Picking a mode blind is how this feature becomes either useless or unbearable, and with no per-tool escape in v1 the preview is the only thing between a mode and an unpleasant surprise: `writes` on a Composio toolkit catches genuine reads whose slug carries a mutation token (`resolve_address`, `get_or_create_customer`).
Computed with :func:`~agentdepot_core.tools.mode_covers` — the same rule the gateway enforces with — so the preview cannot promise something the runtime does not do.
`destructive` will read 0 for most connections and that is honest, not broken: it counts only tools whose server DECLARED `destructiveHint`, and Composio and the native connectors declare nothing, ever.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | -------------------------- |
| 200 | Successful Response | `DecisionCoverageResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/tool-sources/scopes`
[Section titled “GET /api/orgs/{org\_id}/tool-sources/scopes”](#get-apiorgsorg_idtool-sourcesscopes)
List Grantable Scopes
Return the set of coarse capability/integration scopes grantable to an agent.
```plaintext
Enumerates all org tools via the same gateway discovery path used by
``GET /tool-sources/tools``, then groups them by ``core``/``scope`` to
produce a short list of toggles the UI renders in the agent scope selector.
**Core group** — always-on tools (``ToolDefinition.core == True``). Shown
read-only; never stored in ``Agent.allowed_tools``.
**Scope items** — one item per distinct non-null scope string encountered
across all non-core tool definitions. Each item carries the scope string
(the value to store in ``Agent.allowed_tools`` when toggled on), a human
label, the source kind discriminator, and the tool count + slug list for
tooltip/expand.
```
`agent_id` (uuid or slug) scopes the answer to one agent. Pass it whenever the caller is configuring a specific agent: the catalog then reflects what *that* agent can reach, including its owner’s private connections when it runs as its owner. Omit it for the org-wide catalog, which sees shared connections only.
```plaintext
Requires org membership (JWT bearer or ``agd_*`` API token).
```
**Parameters**
| Name | In | Type | Required | Description |
| --------- | ----- | --------------- | -------- | ----------- |
| agent\_id | query | `string` | no | |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ------------------------- |
| 200 | Successful Response | `GrantableScopesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/tool-sources/tools`
[Section titled “GET /api/orgs/{org\_id}/tool-sources/tools”](#get-apiorgsorg_idtool-sourcestools)
List Tools
List all individual tools available for an org.
Delegates to :class:`~agentdepot_agents.tool_gateway.gateway.ToolGateway` for unified discovery across system, custom, upstream MCP, and native integration tool sources. Requires org membership.
The catalog covers the connections the caller can actually use: org-shared ones plus their own private ones (:func:`_viewer_user_id`). It is what the connections page filters by `source_key` to fill a connection’s Tools card, so a viewer-blind read here renders as “No tools exposed” on a healthy private connection.
The response is additive-compatible: `name`, `source`, and `description` are byte-identical to the pre-gateway shape; `slug`, `backend_ref`, `access_level`, `is_terminal`, and `is_pausable` are new additive fields.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ToolListResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### POST `/api/orgs/{org_id}/tool-sources/tools/refresh`
[Section titled “POST /api/orgs/{org\_id}/tool-sources/tools/refresh”](#post-apiorgsorg_idtool-sourcestoolsrefresh)
Refresh Tools
Force-refresh the org’s upstream MCP tool cache and return the full tool list.
Reconnects to all the org’s upstream MCP servers, re-enumerates their tools, and persists the refreshed definitions to the per-integration cache. Returns the same tool list shape as `GET /tool-sources/tools`.
Per-server failures inside enumeration are isolated — a dead server simply contributes no tools and its prior cache is left untouched; the endpoint still returns 200 with whatever tools resolved. `refreshed` therefore carries the per-server account: which servers were dialed, which answered, and what each contributed. An empty `refreshed` means nothing was contacted, which a bare 200 cannot say.
Unlike background discovery, this path ignores the stored health verdict and dials every upstream: a human pressing “refresh” is asking for exactly the re-test the verdict would suppress. A server that answers has its verdict corrected to `ok` on the spot.
“Every upstream” means every one the *caller* can see — org-shared plus their own private connections (:func:`_viewer_user_id`). Passing no viewer is what made this endpoint a no-op on a private connection: the row was filtered out before anything dialed, so the button reported success having contacted nothing and the Tools card stayed empty.
Requires org membership (JWT bearer or `agd_*` API token).
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ---- | --------------- | -------- | ----------- |
| org\_id | path | `string (uuid)` | yes | |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `ToolRefreshResponse` |
| 422 | Validation Error | `HTTPValidationError` |
# Usage
> REST API reference for usage.
All paths below are relative to the base URL `https://api.agentdepot.org` — for example `GET https://api.agentdepot.org/api/orgs/{org_id}/usage/cost`. Authenticate with an `Authorization: Bearer ` header. See the [REST API reference](/docs/reference/rest/) for details.
### GET `/api/orgs/{org_id}/usage/cost`
[Section titled “GET /api/orgs/{org\_id}/usage/cost”](#get-apiorgsorg_idusagecost)
Get Usage Cost
COGS breakdown — byo provider spend vs platform cost. **Admin/owner only** (dollar figures are org-sensitive). Checked against the path org, not a header, so it can’t be bypassed with a mismatched `X-Org-Id`.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ---------------------------------- |
| org\_id | path | `string (uuid)` | yes | |
| window | query | `string` | no | Window: 24h, 7d, 30d (default 30d) |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ----------------------- |
| 200 | Successful Response | `CostBreakdownResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/usage/series`
[Section titled “GET /api/orgs/{org\_id}/usage/series”](#get-apiorgsorg_idusageseries)
Get Usage Series
Time series for one metric, oldest→newest. 400 on an unknown/derived metric (e.g. `storage_bytes` — use its component metrics) or bad granularity.
**Parameters**
| Name | In | Type | Required | Description |
| ----------- | ----- | --------------- | -------- | -------------------------------------------- |
| granularity | query | `string` | no | One of (‘day’, ‘month’) |
| metric | query | `string` | yes | Rollup metric value (see the metric catalog) |
| org\_id | path | `string (uuid)` | yes | |
| window | query | `string` | no | Window: 24h, 7d, 30d (default 30d) |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | --------------------- |
| 200 | Successful Response | `UsageSeriesResponse` |
| 422 | Validation Error | `HTTPValidationError` |
### GET `/api/orgs/{org_id}/usage/summary`
[Section titled “GET /api/orgs/{org\_id}/usage/summary”](#get-apiorgsorg_idusagesummary)
Get Usage Summary
Current usage for an org: counters summed over the window, gauges at their latest reading (window-independent), derived `storage_bytes`, and cost totals.
`gauges_as_of` stamps when that latest gauge reading was taken — the gauges trail live reality by up to an hourly cron cycle, so a client must render it rather than present them as a current count.
Empty while metering is dark — no rollups yet means zeroed maps.
**Parameters**
| Name | In | Type | Required | Description |
| ------- | ----- | --------------- | -------- | ----------------------------------------------- |
| org\_id | path | `string (uuid)` | yes | |
| window | query | `string` | no | Counter/cost window: 24h, 7d, 30d (default 30d) |
**Responses**
| Status | Description | Body |
| ------ | ------------------- | ---------------------- |
| 200 | Successful Response | `UsageSummaryResponse` |
| 422 | Validation Error | `HTTPValidationError` |