Inboxes
GET /api/orgs/{org_id}/inboxes
Section titled “GET /api/orgs/{org_id}/inboxes”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”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 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}”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 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”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”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}”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 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”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”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 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”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”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”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”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”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”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 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}”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”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”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”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 |