# Cadreen Documentation Index > Base URL: https://api.cadreen.com. Every route answers there: developer routes under `/api/v1/cadreen`, the OpenAI-compatible routes under `/v1` (or with the base URL `https://api.cadreen.com/api/v1/cadreen`), A2A under `/a2a` and the agent card at `/.well-known/agent-card.json`. Authenticate with `Authorization: Bearer sk_cadreen_...`. The website (https://cadreen.com) serves the docs; it is not the API. This file provides a structured index of Cadreen API documentation for LLMs and agents. For full content, visit the HTML docs at https://cadreen.com/docs. ## Setting Cadreen up for a person If a person asked you to set Cadreen up for them (the prompt on https://cadreen.com sends you here): - Read this file first, then the pages it links to. Use only what they say. If they don't say it, it isn't there yet: tell the person rather than guess. - Ask what they want Cadreen to handle, show them a short plan, and wait for their yes before you change anything. - The person makes the API key in the dashboard (https://cadreen.com/dashboard/settings, as a workspace admin or owner). Keep it in an environment variable. Never print it or commit it. - The SDKs, CLI and MCP server cover only part of Cadreen so far. Use an SDK method only if these docs show it; otherwise call the HTTP route given here. Never invent a method, route or flag. - An API key decides nothing. A signed-in person says yes or no to a decision in the dashboard: send them there with the decision's `deep_link`. - When you finish, tell the person what you set up, how you checked it, and what's left for them. ## Follow an operation The operation reads use the shared root operations namespace, not a Cadreen-prefixed path: - `GET /api/v1/operations` lists operations for the authenticated caller. - `GET /api/v1/operations/{id}` reads the saved operation state and links to evidence. - `GET /api/v1/operations/{id}/receipt` returns the evidence-graded result projection. - `GET /api/v1/operations/{id}/story` returns ordered beats from directly linked records. - Reads require authenticated caller context. Unknown and foreign IDs are indistinguishable. - Receipt evidence levels are `confirmed`, `observed`, `inferred`, `unknown`, `failed`, and `cancelled`. - A completed internal operation is not proof of outside-world confirmation. Missing story evidence is not inferred from timestamps, text, or trace IDs. - TypeScript, Python, and Go SDK resources expose receipt and story reads for these exact routes. See https://cadreen.com/docs/operations for the list/detail journey and receipt and story examples. Use the operation detail response for saved state; receipt and story provide separate evidence. ## Home (`/desk`): three questions for your workspace - `GET /api/v1/cadreen/desk` answers: what happened while you were away (operations that finished since `since`, default the last 24 hours), what needs you (decisions you may make and operations paused on a person, the one waiting longest first), and what we do not know (operations whose receipt is `unknown` or `inferred`, and decisions whose outcome could not be confirmed). - Each answer has a count and its first three items. Each item has one plain sentence, its operation or decision id, and a deep link to its page. - A member does not see decisions only an admin can make. An API key decides nothing, so it sees what needs a person. ## MCP tools for operations and decisions The MCP server (`/api/v1/cadreen/mcp/sse`) offers read-only tools that answer exactly what the HTTP routes answer the same caller: `cadreen_operations_list`, `cadreen_operations_get`, `cadreen_operations_receipt`, `cadreen_operations_story`, `cadreen_decisions_list` and `cadreen_decisions_get`. No tool makes a decision: a decision's `deep_link` is the page where a person decides. ## Build the path - Relay for existing software: https://cadreen.com/docs/relay — software asks before it acts; rules decide, a person approves, the receipt records it. - Router: https://cadreen.com/docs/router — one job, many services: try it with the built-in test service and no other accounts, a first run, what comes back, a job's shape and how each service speaks it, amounts, why Router switches or stops, what a run did, run events by webhook, and setup with keys and scopes. - Decisions: https://cadreen.com/docs/decisions — a person reviews consequential work. - Routines: https://cadreen.com/docs/routines — review a method before reuse. - Train your own model: https://cadreen.com/docs/training — switch a trained model on for a share of requests, switch it off, or download its weights; a person decides each step. - Devices and Edge: https://cadreen.com/docs/edge — with offline and safety limits. ## Relay for existing software Relay is for software you can't rebuild around Cadreen (an ERP, a billing system, a script). It is a different product from the OpenAI-compatible chat endpoint. Full page, with curl: https://cadreen.com/docs/relay - It is cooperative. Software that asks first can't act without a permit that names exactly what it asked for; Relay can't stop software that never asks, and the person who approves may be the developer whose key asked. - Every request needs `Authorization: Bearer `. Ask, redeem, report and send-an-event also need an `Idempotency-Key` (1 to 255 bytes); a dry run takes none (sending one is refused), and a question needs none. Bodies are JSON, at most 64 KB; numbers that aren't exact in 64 bits, and money, go as strings. Read `error.code`, never the message. Any refusal means: don't act. - Ask: `POST /api/v1/cadreen/relay/ask` with `{action, arguments}` (or `{service, arguments}`). The answer is `allow` with a `permit` (200), `deny` (200: the rules refuse it, don't act) or `held` (202: a person decides; poll `GET /api/v1/cadreen/relay/actions/{operation_id}`). A permit is bound to the exact arguments, lasts 15 minutes and is used once. - Redeem just before acting: `POST /api/v1/cadreen/relay/permits/redeem` with `{permit, arguments}`. Report once: `POST /api/v1/cadreen/relay/actions/{operation_id}/outcome` with `{status: "done" | "failed" | "unknown", evidence?}`. `unknown` opens a "Did this happen?" question for a person. - Hear back: `GET /api/v1/cadreen/relay/events?after=&limit=<1..500>` lists every move of your asks oldest first, never skipping one, for 30 days. Or an admin signed in as a person subscribes a signed, retried webhook with `POST /api/v1/cadreen/relay/webhooks` (an API key can't). - Send events: an admin drafts the list of event types your software may send (`POST /api/v1/cadreen/relay/manifests`) and approves it as a decision. Check a sample with `POST /api/v1/cadreen/events/dry-run` (records nothing), then send `POST /api/v1/cadreen/events` with `Idempotency-Key: :`. A kind of event is kept as a record or, if it runs a tool, held for a person unless an admin has let that kind run by itself, up to a daily limit; a rule can also refuse it. Events need an events key (made in Settings, under API keys, as "Send events only"): it sends and dry-runs events and reads what it needs to check them (the list of events in force and the key that checks its seal), and the change feed, one operation and its receipt for its workspace's Relay work. A sidecar's events key also reports that sidecar's health and what it found and counted in the software's database, reads the mapping it was given, and reads and reports the remote sources it is given (`POST /api/v1/cadreen/relay/sidecar/heartbeat`, `POST /api/v1/cadreen/relay/sidecar/discovery`, `GET /api/v1/cadreen/relay/sidecar/discovery-mapping`, `POST /api/v1/cadreen/relay/sidecar/observations`, `GET /api/v1/cadreen/relay/sidecar/remote-sources`, `POST /api/v1/cadreen/relay/sidecar/remote-sources/state`). It can't ask, chat or change anything else, and a Relay key can't send events. - Ask the rules a question without asking to act: `POST /api/v1/cadreen/decide` with `{question: "allowed", action, attributes}` answers `yes`, `needs_person` or `unknown`. Treat `unknown` as don't act. - A Relay key (`sk_relay_…`, made by an admin with `kind: "relay"` on the API-key route) can ask, redeem, report, read its asks and the feed, read the key that verifies permits offline (`GET /api/v1/cadreen/relay/keys`), and read the change feed, one operation and its receipt for its workspace's Relay work. It also reports its sidecar's health and what its sidecar found and counted in the software's database, reads the mapping it was given and its remote sources and reports their state (`POST /api/v1/cadreen/relay/sidecar/heartbeat`, `POST /api/v1/cadreen/relay/sidecar/discovery`, `GET /api/v1/cadreen/relay/sidecar/discovery-mapping`, `POST /api/v1/cadreen/relay/sidecar/observations`, `GET /api/v1/cadreen/relay/sidecar/remote-sources`, `POST /api/v1/cadreen/relay/sidecar/remote-sources/state`), and, where actions are set up, declares and runs its sidecar's actions (`PUT /api/v1/cadreen/relay/sidecar/actions`, `GET /api/v1/cadreen/relay/sidecar/acts/next`, `POST /api/v1/cadreen/relay/sidecar/acts/redeem`, `POST /api/v1/cadreen/relay/sidecar/acts/{id}/outcome`), on the same sidecar routes. It can't do anything else. - Not connected yet: Message queues and mainframes (IBM MQ, Kafka, CICS); Email in, desktop programs and terminal screens. Partly connected: Software that can send a JSON event; MySQL, SQL Server and Oracle databases; CSV files from a legacy system; Fixed-width, EBCDIC and X12 files; Files fetched from an SFTP folder; Hospital systems that send HL7 v2 messages (MLLP); SOAP and WSDL services. Cadreen acts inside your network only through a paired sidecar, for one action set up in the sidecar's own settings (RELAY_SIDECAR_ACT=on; one write to a PostgreSQL or SQLite request table in this release), approved by a person each time. ## Quickstart Example Send intent as a chat message. The system classifies, checks capabilities, evaluates governance, and returns a typed result: ``` POST /api/v1/cadreen/intent Authorization: Bearer sk_cadreen_... { "messages": [ {"role": "user", "content": "Block EU customer data transfers to US servers without adequacy decision"} ], "user_id": "cust_12345" } → 200 { "type": "clarify", "message": { "role": "assistant", "content": "I can help you build compliance monitoring...", "metadata": {"cadreen_type": "clarify"} }, "clarification": { "questions": ["Which jurisdictions do you operate in?", "Do you have SCCs documented?"], "conversation_id": "conv_01h9x..." }, "intelligence": { "version": "1.0", "capability": {"total_available": 3, "healthy_count": 1}, "governance": {"active": true, "decision": "clarify_requester"}, "process": {"duration_ms": 342} } } ``` One endpoint. Three outcomes: clarify (asks questions), direct (answers now), execution (runs a multi-step task with tools). ## Authentication All requests require an API key in the Authorization header. Keys are prefixed with `sk_cadreen_` and scoped to a workspace. ``` Authorization: Bearer sk_cadreen_... ``` - Keys are created via `POST /api/v1/maas/workspaces/{id}/api-keys` by a signed-in workspace admin or owner - Changing rules (policies) with a key needs its `policies.write` scope; setting up routing jobs needs `routing.setup`; connecting a service to the workspace needs `connections.write`. Without it: 403 `scope_required` - A key whose permissions don't include write can only read: `GET` and `HEAD`, except the GETs that run a model and are charged, and the calls that only look something up. Everything else is refused before it runs: 403 `api_key_write_required` (nothing was changed or charged), or `api_key_permissions_unreadable` when the key's permissions can't be read. Read-only means read-only, and a scope never widens it: a key that lists read and a scope such as `policies.write` can't use the scope. Every key made today can write - Calls beyond GET and HEAD that a read-only key can make (also under `/cadreen`, without `/api/v1`): `POST /api/v1/cadreen/agents/{agentID}/knowledge/search`, `POST /api/v1/cadreen/policies/evaluate`, `POST /api/v1/cadreen/policies/simulate`, `POST /api/v1/cadreen/routing/dryRunByName`, `POST /api/v1/cadreen/routing/groups/{id}/dry-run` and `POST /api/v1/cadreen/mcp/message`. The MCP endpoint checks each tool's own level: `cadreen_devices_ask`, `cadreen_intent` and `cadreen_responses_create` run a model, so they need write. A tool the key can't use answers with `api_key_write_required` and "This key can only read. Use a key that may write to do this." and is not run - GETs that a read-only key can't make, because each runs a model and is charged (also under `/cadreen`): `GET /api/v1/cadreen/learning/episodes`, `GET /api/v1/cadreen/learning/patterns`, `GET /api/v1/cadreen/learning/suggestions` and `GET /api/v1/cadreen/routing/groups/{id}/discover` - What a read-only key still leaves behind: A key that can only read changes nothing in your data and spends nothing. Cadreen still keeps its own record of what it was asked: when the key was last used, a trace of each search, policy check or routing dry run that answers with a result, and a row in its error log for a call that fails after reaching its route. A POST sent with an Idempotency-Key header also has its answer kept for 24 hours, so that sending it again returns the same answer (a cache of the answer, not your data). The trace and the row say which key made the call (the log keeps a limited number an hour for each key). - Reads that set something up the first time they are used, which a read-only key can make (a cache or a registration, not something you stored): the `GET` calls under `/api/v1/cadreen/devices/` except `/api/v1/cadreen/devices/mission/{missionId}` and the MCP tools `cadreen_devices_` that only look something up create the workspace's device files the first time a workspace uses any of them - Keys are shown once — store securely - Rate limits are per workspace. Read the `Retry-After` header on 429 responses - Data never crosses workspace boundaries ## User Scoping Pass `user_id` in the request body to link a request to a specific end user. Cadreen uses it to scope memory, conversations, and history to that person. ```json { "messages": [{"role": "user", "content": "..."}], "user_id": "your-customer-id" } ``` - Optional — omit for shared/workspace-level context - Free-form string (your internal ID, Auth0 sub, etc.) - Scopes knowledge items, conversation history, and preferences per user - Each workspace is isolated by default; user_id adds per-user isolation within a workspace ## Main Documentation - https://cadreen.com/docs — Main HTML documentation (includes API reference section) - https://cadreen.com/docs/md — Markdown version ## Concepts Cadreen is the orchestration around models — not a chat API wrapper. The core building blocks: - **Memory:** Cadreen remembers things. Facts, decisions, observations — structured, versioned, searchable across sessions. It accumulates knowledge the more you use it. - **Governance:** You set rules. Cadreen follows them. Policies evaluate every action before it runs. No action happens without your rules approving it first. - **Healing:** When something breaks, Cadreen fixes it. It diagnoses the problem, tries a recovery, and remembers what worked. Next time the same thing breaks, it already knows the answer. - **Audit:** A successful answer shows you what happened and why. A full trace of what the system knew, what rules it checked, and how it decided what to do. - **Learning:** Cadreen watches what works. It spots patterns in your usage, proposes improvements, and suggests things to automate. Nothing changes without your approval. - **Escalations:** When something needs your judgment, Cadreen asks. It doesn't guess — it pauses and brings you in with full context. - **Connections:** Any tool with an API becomes part of the system. Connect Stripe, GitHub, Salesforce, or your own internal tools — Cadreen orchestrates them together. - **Suggestions:** Cadreen proposes things it thinks you should do — recurring actions to automate, schedules to set, rules to relax. You decide what runs. Dismissing teaches it what you don't want. ## Common Patterns ### Pattern 1: Store knowledge → Query → Get trace ``` POST /api/v1/cadreen/memory {"type": "reference", "content": {"text": "..."}, "authority": 10} GET /api/v1/cadreen/memory/search?query=... GET /api/v1/cadreen/intelligence/{trace_id} ``` ### Pattern 2: Create policy → Evaluate → Confirm ``` POST /api/v1/cadreen/policies {"name": "policy-name", "applies_to": "tools", "rules": [{"condition": "Describe when it applies, in plain English", "effect": "require_human_approval"}]} POST /api/v1/cadreen/policies/evaluate {"action": "describes what to check"} POST /api/v1/cadreen/policies/{id}/confirm ``` ### Pattern 3: Assess → Connect → Deploy ``` POST /api/v1/cadreen/intent {"messages": [{"role": "user", "content": "describe your task"}]} ← Response tells you what capabilities are missing and what to connect POST /api/v1/cadreen/connections/openapi {"name": "my-api", "spec_url": "..."} POST /api/v1/cadreen/intent ← re-assess with new connections ``` ### Pattern 4: Execute → Stream → Audit ``` POST /api/v1/cadreen/executions {"intent": "..."} → returns execution_id GET /api/v1/cadreen/executions/{id}/stream → SSE events GET /api/v1/cadreen/audit/{execution_id}/timeline → full audit trail ``` ### Pattern 5: Handle intent outcomes (the complete result algebra) Every `POST /api/v1/cadreen/intent` call returns a discriminated union. Branch on `status`, not `type`: | Status | Meaning | What to do next | |--------|---------|------------------| | `answered` | Direct answer, no further action needed | Use `message.content` | | `needs_input` | System needs clarification | Present `clarification.questions` to the user | | `ready_to_execute` | Execution plan approved, awaiting confirmation | Confirm via execution endpoint | | `executing` | Execution is running | Stream events or poll status | | `planning` | An execution was admitted and is being planned. Nothing runs until someone with control of the execution starts it: a person in its workspace, or an API key acting in that workspace | Read the execution at `next_action.endpoint` (`wait_for_plan`) | | `blocked_by_policy` | Governance policy blocked the action (a 403 whose body is the answer, not an error) | Read `meta.governance` (`decision`, `reason`); `intelligence.governance` names the rule when the answer carries one; a person decides (`next_action`) | | `blocked_by_missing_connection` | Required tool not connected (also a 403 whose body is the answer) | Follow `next_action.endpoint` to connect | | `failed` | Unrecoverable error | Retry with backoff or escalate (`next_action.type` is `retry_or_escalate`); `intelligence` has the trace when the answer carries one | A `next_action` is there when the server has something for your code to do: the status sets one for `needs_input`, `ready_to_execute`, `planning`, `blocked_by_policy`, `blocked_by_missing_connection` and `failed`, the call's trace can set another, and an `answered` or `executing` call has none unless the trace made one. Its `type` is one of thirteen values: | next_action.type | When it appears | What your code should do | |------------------|----------------|--------------------------| | `provide_input` | Status `needs_input` | Present `clarification.questions` to the user and send the answers | | `confirm_execution` | Status `ready_to_execute`, for an execution that has not started | Ask the user to confirm before executing | | `wait_for_plan` | Status `planning` | GET `next_action.endpoint` (the execution) until it reads `awaiting_approval`; then a person approves the plan on the execution's page, or a key of that workspace calls `POST /api/v1/missions/{id}/start`. Nothing runs before that | | `start_mission` | Auto mode's offer: the plan is the answer | Send the same request to `next_action.endpoint` with its `method` and `mode` to start the plan. Nothing runs until it is taken, and taking it is charged | | `resolve_escalation` | Status `blocked_by_policy` | A person decides the block | | `connect_tool` | A tool is missing: a gap that does not block, or status `blocked_by_missing_connection` | Call the connections endpoint in `next_action.endpoint` | | `resolve_gap` | A gap blocks the call | Redirect the user to connections | | `add_policy` | A rule holds or blocks the call | Add or change a rule | | `add_memory` | The memory store is not healthy | Tell the user memory is unavailable; retry later | | `create_escalation` | In a trace's `next_actions`, when a rule needs a person | Ask a person. The trace names `/api/v1/cadreen/escalations`, which lists and resolves escalations and has no route that creates one | | `revise_intent` | In a trace's `next_actions`, when a rule needs a person | Prompt the user to change the request | | `retry_or_escalate` | Status `failed` | Retry with backoff or escalate | | `none` | Governance approved it and nothing blocks | Nothing: this is terminal | A successful answer that is a JSON object, from a route that runs the intelligence layer (most of `/api/v1/cadreen`, `POST /api/v1/intent` and the `/api/v1/executions` routes; the introduction of the OpenAPI spec lists the ones that do not) carries `trace_id`, and an `intelligence` object only when asked for or when the route decides. The decision routes (`POST /intent`, `POST /policies/evaluate`, `POST /assess`) carry a short block: the route's own `decision` for the call and its `policy_id` (each left out when the route gives none: it is never a summary of your rules), `next_actions` and, when a gap analysis ran, `blocking_gaps`. `?include=intelligence` (or `Accept: application/json; profile="full"`) returns the full reasoning trace on any route: capability matches, governance decisions, memory lookups, gaps detected and process timing. `profile="lean"` is what every other route gets without asking (a route that writes its own `intelligence` object keeps it, as chat completions and intent do when the engine made one; a decision route's is cut down to the short block) and `profile="audit"` sends the part that bears on an action. The stored trace is at `GET /api/v1/cadreen/intelligence/{trace_id}`. An error and a CSV export carry none. A stream carries no `trace_id`; a chat completions stream sends `intelligence` in its last data event when the model's answer is streamed, and none when the call is blocked, answered with a question or a mission, or ends in a fallback. A block is a 403 whose body is the answer: its `intelligence` is the engine's own trace when it made one, and it carries no `trace_id` in the body (read the `X-Cadreen-Trace-ID` header). ## Integration Methods Connect external tools via three protocols: - **OpenAPI:** Register an OpenAPI spec URL. Every operation becomes an executable tool. - **MCP:** Register a Model Context Protocol server (SSE transport). - **Marketplace:** Browse and install from 30+ pre-built integrations (Slack, Stripe, Gmail, etc.). ## MCP Server (Cadreen as MCP provider) Cadreen exposes itself as an MCP server over SSE. Any MCP-compatible client (Cursor, Claude Desktop, Continue) can connect directly. - **SSE endpoint:** `GET /api/v1/cadreen/mcp/sse` — Opens SSE stream, receives endpoint URL - **JSON-RPC endpoint:** `POST /api/v1/cadreen/mcp/message?session_id=xxx` — Send JSON-RPC requests - **Auth:** `Authorization: Bearer sk_cadreen_...` - **57 tools** (the answer of `tools/list`, which has each tool's description and input schema): `cadreen_agents_create`, `cadreen_agents_deploy`, `cadreen_agents_get`, `cadreen_agents_list`, `cadreen_bci_connect`, `cadreen_bci_disconnect`, `cadreen_bci_policy_get`, `cadreen_bci_policy_update`, `cadreen_bci_status`, `cadreen_bci_stream_start`, `cadreen_bci_stream_stop`, `cadreen_decisions_get`, `cadreen_decisions_list`, `cadreen_devices_ask`, `cadreen_devices_assign`, `cadreen_devices_avoidance`, `cadreen_devices_blackboard`, `cadreen_devices_collisions`, `cadreen_devices_create`, `cadreen_devices_delete`, `cadreen_devices_diagnose`, `cadreen_devices_get`, `cadreen_devices_grid`, `cadreen_devices_grid_stats`, `cadreen_devices_list`, `cadreen_devices_stats`, `cadreen_devices_status`, `cadreen_devices_sync_conflicts`, `cadreen_devices_sync_pending`, `cadreen_devices_sync_status`, `cadreen_devices_tasks_add`, `cadreen_devices_tasks_complete`, `cadreen_devices_tasks_list`, `cadreen_devices_update_map`, `cadreen_devices_update_state`, `cadreen_external_agents_connect`, `cadreen_external_agents_delete`, `cadreen_external_agents_get`, `cadreen_external_agents_get_settings`, `cadreen_external_agents_list`, `cadreen_external_agents_list_all`, `cadreen_external_agents_list_interactions`, `cadreen_external_agents_revoke`, `cadreen_external_agents_suspend`, `cadreen_external_agents_update_settings`, `cadreen_federation_create`, `cadreen_federation_list`, `cadreen_governance_create`, `cadreen_governance_list`, `cadreen_intent`, `cadreen_knowledge_add`, `cadreen_knowledge_search`, `cadreen_operations_get`, `cadreen_operations_list`, `cadreen_operations_receipt`, `cadreen_operations_story`, `cadreen_responses_create` - **Protocol version:** 2024-11-05 Alternatively, install the stdio MCP server: `npm install -g @cadreen/mcp` ## Error Handling Most error responses follow one structure: `{"error": {"type", "code", "message"}}`. Parse `error.code` (stable), not `error.message` (may change). A few routes answer a flatter body, `{"error": "", "message": "…"}`, where `error` is the code itself (most errors of the intent routes and of the BCI routes): read `error.code` when `error` is an object, and `error` when it is a string. | HTTP | Code | Meaning | Fix | |------|------|---------|-----| | 401 | missing_authorization_header, invalid_authorization_header_format | No `Authorization` header, or one that is not `Bearer ` | Send `Authorization: Bearer sk_cadreen_...` | | 401 | invalid_api_key, api_key_expired, api_key_revoked | The key does not work | Use a valid key; create a new one if it expired or was revoked | | 400 | workspace_required | A signed-in person who belongs to several workspaces chose none | Send the one you mean in the `X-Workspace-ID` header (a workspace API key already names one) | | 403 | workspace_membership_required | A signed-in person who is not in a workspace yet | Ask someone to add you | | 429 | rate_limited | Too many requests | Read `Retry-After` header; back off | | 500 | internal_error | Server-side failure | Retry with exponential backoff; report request ID if persistent | | 503 | service_unavailable | Downstream dependency unavailable | Check `/cadreen/health`; retry after recovery | The sign-in refusals and the rate limiter answer in the error body, not the flat one: `{"error": {"type": "rate_limit", "code": "rate_limited", "message": "…", "next": "…", "doc_url": "…", "retry_after": 3, "details": {"category": "default"}}}`, with a `Retry-After` header (the 401 `missing_authorization_header` has the same fields without `retry_after`). On the intent routes a block by a policy is not an error: the call answers 403 with the answer as its body (`type` is `blocked`, `status` is `blocked_by_policy`), and `meta.governance.decision` and `meta.governance.reason` say why; a stream sends it as an event in a 200 response. Read it as an answer, not as an error. ## API Reference ### System - `GET /api/v1/cadreen/health` — System health check. Returns `status`, `memory`, `connections`, `governance`, `intelligence`. - `GET /api/v1/cadreen/docs/openapi.json` — Machine-readable OpenAPI 3.1 spec. Full schema for every endpoint, parameter, and response. ### Workspaces - `POST /api/v1/maas/workspaces` — Create a workspace (isolates all data, policies, and connections). An owner or admin of your organization's default workspace, signed in as a person; API keys can't. You become its owner. The first workspace of an organization that has none is anyone's to make - `POST /api/v1/maas/workspaces/{id}/api-keys` — Generate an API key (one-time plaintext reveal). A signed-in admin may add `scopes`: `policies.write` (change rules) - `GET /api/v1/maas/workspaces/{id}/api-keys` — List all API keys for a workspace - `POST /api/v1/maas/workspaces/{id}/api-keys/{keyId}/rotate` — Rotate an API key (creates new, revokes old) - `DELETE /api/v1/maas/workspaces/{id}/api-keys/{keyId}` — Revoke an API key ### Connections Browse and install integrations from the unified marketplace, or register custom connectors: - `GET /api/v1/cadreen/connections/catalog` — Browse the integration marketplace. Returns the curated integrations grouped by category (Communication, Payments, Project Management, etc.), each with whether it can be connected on this server (`connectable`; `status` not_configured when this server isn't set up to connect it, coming_soon when it can't be connected yet), and what you have installed. `total_available` counts the ones that can be connected here. - `POST /api/v1/cadreen/connections/install` — One-click install an integration. Pass `integration_id` (e.g. `"slack"`, `"github"`). Returns an OAuth URL for authentication. - `GET /api/v1/cadreen/connections` — List all installed connections - `POST /api/v1/cadreen/connections` — Register a generic connection by capability name - `DELETE /api/v1/cadreen/connections/{id}` — Unregister a connection **Marketplace:** - `GET /api/v1/cadreen/connections/catalog` — Browse the unified integration catalog - `POST /api/v1/cadreen/connections/install` — One-click install an integration **Advanced (provider-specific — prefer marketplace endpoints above):** - `POST /api/v1/cadreen/connections/openapi` — Register an OpenAPI spec - `POST /api/v1/cadreen/connections/mcp` — Register an MCP server **Connection Sessions (custom connections):** 1. Create a session. Cadreen searches existing integrations and reports `discovery.outcome`: `found`, `not_found`, or `incomplete` (a provider could not be searched, so "nothing found" would not be true; `discovery.providers` says which). Nothing is built and the service is not researched yet. 2. Choose: select an existing path, or build a custom connection with `POST .../forge` and the API's `url` (or its specification's `spec_url`; an empty body confirms the address the session has). 3. The build runs research, design, build. The session's `plan` lists every operation (method, real path, `origin`: `spec`, `confirmed` or `draft`, risk, whether each call needs approval), the auth requirement and the test result. 4. Drafted operations wait for you: name each once with `POST .../plan/confirm`, in `confirm` (kept) or `remove` (dropped). Nothing is tested or deployed while a draft remains. If the API needs a credential, the session waits in `awaiting_credentials`; attach one of yours with `POST .../credentials`. 5. A workspace admin approves (`POST .../deploy`) or declines (`POST .../reject`). This is the same decision as `connection_deploy:{id}` in the decision inbox; API keys can't decide. After deploy the session is `observing` until its health is measured with a real read-only call. When this server has no health check, `health.status` is `not_measured`: nothing claims the connection works or doesn't. 6. To remove a connection that was added, `POST .../disconnect`: every tool it added is removed first, then the session is `cancelled` with a `disconnected` event. If removal can't be confirmed, the session is unchanged; repeating is safe. Calls to private or internal addresses are refused unless the operator lists the host in `MERIDIAN_OUTBOUND_PRIVATE_HOSTS`. Writes by an API key need its `write` permission when its permissions are scoped. `available_actions` lists the steps the current state offers. Events carry a `seq`; the stream sends it as the SSE id, so reconnecting with `Last-Event-ID` misses nothing. - `POST /api/v1/cadreen/connections/sessions` — Create a connection session and search existing integrations - `GET /api/v1/cadreen/connections/sessions` — List this workspace's sessions - `GET /api/v1/cadreen/connections/sessions/{id}` — Get session details, discovery result and plan - `GET /api/v1/cadreen/connections/sessions/{id}/events` — List session events in order (`after_seq`, `limit`) - `GET /api/v1/cadreen/connections/sessions/{id}/stream` — SSE stream for live updates (`Last-Event-ID` or `after_seq` to resume) - `POST /api/v1/cadreen/connections/sessions/{id}/select` — Select an existing path - `POST /api/v1/cadreen/connections/sessions/{id}/forge` — Build a custom connection (`url` or `spec_url`); after a failed deploy, ask for approval again - `POST /api/v1/cadreen/connections/sessions/{id}/plan/confirm` — Confirm or remove drafted operations (`confirm`, `remove`) - `GET /api/v1/cadreen/connections/sessions/{id}/credentials` — The saved credentials this connection may attach: only those saved in this workspace (never a tenant-wide one). Names and providers, never a secret - `POST /api/v1/cadreen/connections/sessions/{id}/credentials` — Attach credentials saved in this workspace (`credential_ids`; a tenant-wide or another workspace's credential answers 404 `credential_not_found`), or store a new one, encrypted, as this workspace's only, and attach it (`credential`: `token`; `username` and `password`; or `api_key`; or `client_id` and `client_secret` for an API that signs a person in at its own page) - `POST /api/v1/cadreen/connections/sessions/{id}/oauth/start` — For an API that signs a person in at its own page (OAuth 2): once the app's client ID and secret are saved for the connection, an owner or admin signed in as a person gets the address to sign in at (good for 10 minutes, works once). The sign-in comes back to the server by itself and testing begins. 409 `not_waiting_for_sign_in` until the app is saved - `POST /api/v1/cadreen/connections/sessions/{id}/verify` — Trigger verification - `POST /api/v1/cadreen/connections/sessions/{id}/deploy` — Approve and deploy (workspace admin) - `POST /api/v1/cadreen/connections/sessions/{id}/reject` — Decline the deploy (workspace admin; `note` is the reason) - `POST /api/v1/cadreen/connections/sessions/{id}/pause` — Pause the search or build - `POST /api/v1/cadreen/connections/sessions/{id}/resume` — Resume a paused session - `POST /api/v1/cadreen/connections/sessions/{id}/cancel` — Cancel session - `POST /api/v1/cadreen/connections/sessions/{id}/disconnect` — Remove a connection that was added (its tools first). Needs a workspace admin signed in as a person: an API key gets 403 `person_required`, a member 403 `admin_required` ### Memory **type vs kind distinction:** `type` is user-settable and always one of 6 public values: `reference`, `preference`, `episode`, `precedent`, `note`, `project`. `kind` is system-assigned by LLM classification or human edit (system-assigned values like `policy`, `observation`, `tool_invocation`, `error`, etc.) and appears in GET responses when deep classification has occurred. `classified` (boolean) is true when the system has deep-classified the item. - `POST /api/v1/cadreen/memory` — Store a knowledge item. Required: `content.text`. Optional: `type` (one of the 6 public types; `reference` when left out), `domain`, `authority`, `tags`, `content.is_private`. `content.is_private: true` keeps the item to the person who saved it: nobody else reads it by ID, finds it by searching or deletes it (not another member, not an admin, not an API key), and the answer says `scope: "personal"`. It needs a signed-in person (an API key gets 403 `person_required`), is not sent to a model to be classified, and a value that is not `true` or `false` is 400 `invalid_request`. Nothing is saved when it is refused. - `GET /api/v1/cadreen/memory/search` — Full-text search over the text and summary of each item (`*` matches everything). Query params: `query`, `domain`, `type`, `tag`, `limit`, `offset`. Returns items with both `type` (public) and `kind` (system-classified). Someone else's private item is not in it. - `GET /api/v1/cadreen/memory/{id}` — Get item by ID. Someone else's private item is 404 `memory_not_found`. - `GET /api/v1/cadreen/memory/profile/{userId}` — User profile (static facts + episodic patterns) - `GET /api/v1/cadreen/memory/types` — Discover valid types/kinds. Returns `type_values` (6 writable categories) and `kind_values` (system-assigned classifications). ### Documents A document is read, approved and signed in the workspace that uploaded it. A document a person uploads on AccomplishAnything is private to them, even inside a shared workspace: a colleague, an admin and an API key get `404 not_found`, the same answer as for a document in another workspace, and nothing overrides it. A document uploaded on Cadreen belongs to the workspace: anyone who can use the workspace's documents can open, approve and sign it. A document that names no workspace is its uploader's alone. An API key needs write permission to upload, approve or sign: a key without write permission is gets `403 api_key_write_required`. - `POST /api/v1/cadreen/documents/upload` — Upload a document (PDF, DOCX, XLSX, PPTX, or plain text, max 50MB). Returns `document_id`, `status`, `filename`, `atoms_extracted`. - `GET /api/v1/cadreen/documents` — List the documents you may read (your own uploads and your workspace library), as an array. - `GET /api/v1/cadreen/documents/{id}` — Get a single document by ID. - `GET /api/v1/cadreen/documents/{id}/download` — Download a document's latest stored file (always as a download). A file that is no longer stored is `410 document_file_missing`. - `POST /api/v1/cadreen/documents/{id}/approve` — Approve the latest version so it can be signed. A person or an API key can approve; `metadata.approver_label` says which (`Approved by Ada Lovelace`, `Approved by key Billing bot`). Optional body: `version_id`, the version you looked at (a version that isn't the latest is `409 version_not_latest`); no other field is accepted. A new version clears the approval. - `POST /api/v1/cadreen/documents/{id}/sign` — Sign an approved document (the approval must be of the latest version). A person or an API key can sign; `metadata.signer_label` says which (`Signed by Ada Lovelace`, `Signed by key Billing bot`). Optional body: `signer_name`, what you typed, kept apart as `metadata.signer_typed_name` and never the name the signature says. ### Intent (unified entry point) - `POST /api/v1/cadreen/intent` — Classify, assess, and route. Accepts `messages` array (chat format), `context` (domain, user), `mode` (auto/chat/execution). Returns one of three types: `clarify`, `direct`, or `execution`. A successful answer carries an `intelligence` trace. ### Assessment - `POST /api/v1/cadreen/assess` — Evaluate task readiness. Returns `can_do`, `capabilities`, `gaps`, `stack`, `policies_recommended`, `ready_for_deployment`. - `GET /api/v1/cadreen/capabilities` — List all available capabilities ### Governance - `GET /api/v1/cadreen/policies` — List active policies - `POST /api/v1/cadreen/policies` — Create a policy bundle (draft) - `GET /api/v1/cadreen/policies/{id}` — Get policy details - `POST /api/v1/cadreen/policies/evaluate` — Evaluate an action against active policies - `POST /api/v1/cadreen/policies/{id}/confirm` — Activate a policy bundle ### Audit - `GET /api/v1/cadreen/audit` — Query the audit actions of your workspace. Filters: `type`, `execution_id`, `limit` (max 200), `offset`. `scope=account` lists the whole account's, the actions that belong to no workspace included: only a person who is an admin or owner of the account may ask (403 `admin_required` for a member, 403 `person_required` for an API key). - `GET /api/v1/cadreen/audit/stats` — Aggregated statistics of your workspace's actions (`scope=account` for the account's, the same rule) - `GET /api/v1/cadreen/audit/{executionId}/timeline` — Full execution timeline with reasoning ### Learning - `GET /api/v1/cadreen/learning/patterns` — Detected patterns, each with how many requests showed it and when the first and the last did (no confidence number: nothing measured one) - `GET /api/v1/cadreen/learning/episodes` — Similar past executions via semantic search - `GET /api/v1/cadreen/learning/suggestions` — Recommended improvements and knowledge gaps Example — detected pattern: ``` GET /api/v1/cadreen/learning/patterns → 200 { "patterns": [{ "id": "2f6c1c1e-8a3a-4a53-9f7e-0d1c6e2b7a11", "type": "user", "description": "Cross-border transfers consistently blocked by data-privacy-governance", "frequency": 23, "evidence": [], "first_seen_at": "2026-06-02T09:14:00Z", "detected_at": "2026-06-09T08:00:00Z", "last_seen_at": "2026-06-08T17:41:00Z", "metadata": {} }], "improvements": [] } ``` Example — similar episodes: ``` GET /api/v1/cadreen/learning/episodes?query=refund%20over%20$500 → 200 { "episodes": [{ "id": "ep_01", "summary": "Refund of $750 escalated to manager — approved after review", "outcome": "resolved", "relevance": 0.91 }] } ``` ### Task Proposals The system detects patterns in your usage and proposes actions for your review. Accepting records your yes: a research proposal runs at once (unless marked for review); any other returns its intent to send to `POST /intent`. Dismissing keeps your reason. A proposal is made once: accepted, dismissed or expired, it isn't made again. Default returns proposals waiting for your decision. - `GET /api/v1/cadreen/proposals` — List task proposals (default: `status=proposed`). Pass `status=all` to see everything. - `GET /api/v1/cadreen/proposals/{id}` — Get a single proposal - `POST /api/v1/cadreen/proposals/{id}/accept` — Accept. A research proposal runs now and returns its result; any other returns the intent to send to `POST /intent`. A signed-in person only: an API key gets 403 `person_required`. - `POST /api/v1/cadreen/proposals/{id}/dismiss` — Dismiss with optional reason. A signed-in person only, as for accept. - `GET /api/v1/cadreen/proposals/stats` — Counts by status Example — list proposals: ``` GET /api/v1/cadreen/proposals → 200 { "proposals": [{ "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Auto-approve refunds under $50 in support", "description": "You've approved 47 refunds under $50 in the last 30 days with 100% success. Want to automate?", "proposal_type": "automation", "confidence": 0.97, "priority": 8, "status": "proposed", "intent": "Auto-approve refunds under $50 in the support domain" }], "count": 1 } ``` Example — accept a proposal: ``` POST /api/v1/cadreen/proposals/550e8400.../accept → 200 { "status": "accepted", "execution_id": "770e8400...", "intent": "Auto-approve refunds under $50 in the support domain", "action": "execute", "next_step": "Call POST /intent with the intent field to execute it" } ``` ### Escalations - `GET /api/v1/cadreen/escalations` — List pending escalations - `GET /api/v1/cadreen/escalations/{id}` — Get escalation detail with full context - `POST /api/v1/cadreen/escalations/{id}/resolve` — Resolve with decision (`auto`, `auto_complete`, `handoff`, `escalate`, `clarify_requester`, `abstain`) ### Healing - `GET /api/v1/cadreen/healing/precedents` — List recovery precedents - `GET /api/v1/cadreen/healing/precedents/{id}` — Get a specific precedent by ID - `GET /api/v1/cadreen/healing/stats` — Aggregated healing statistics - `POST /api/v1/cadreen/healing/diagnose` — Diagnose a failure. Returns root cause with confidence and recovery strategy. - `POST /api/v1/cadreen/healing/record-recovery` — Record a successful recovery as a precedent for future healing - `POST /api/v1/cadreen/healing/preconditions` — Check preconditions before attempting recovery - `GET /api/v1/cadreen/healing/recoveries` — List the recoveries in progress in your workspace, each with the id it is addressed by - `GET /api/v1/cadreen/healing/recoveries/{recoveryId}` — Get a recovery in progress by its id - `DELETE /api/v1/cadreen/healing/recoveries/{recoveryId}` — Stop a recovery that has not acted yet. One that already started its work answers 409 (`recovery_already_acting`) and finishes. ### Credentials - `GET /api/v1/cadreen/credentials` — List credential metadata (names and types only; secret data never exposed) - `POST /api/v1/cadreen/credentials` — Store a credential - `DELETE /api/v1/cadreen/credentials/{id}` — Delete a credential ### Webhooks - `POST /api/v1/cadreen/webhooks` — Subscribe to events. Required: `url` (HTTPS), `events` array, `secret`. - `GET /api/v1/cadreen/webhooks` — List all subscriptions - `GET /api/v1/cadreen/webhooks/{id}` — Get a specific subscription by ID - `DELETE /api/v1/cadreen/webhooks/{id}` — Delete a subscription - `GET /api/v1/cadreen/webhooks/{id}/deliveries` — List delivery attempts with status, timestamp, and response code ### Intelligence (trace querying + actions) - `GET /api/v1/cadreen/intelligence` — List historical intelligence traces. Filters: `from`, `to`, `domain`, `decision`, `min_confidence`. - `GET /api/v1/cadreen/intelligence/stats` — Aggregated trace statistics (30-day window) - `GET /api/v1/cadreen/intelligence/{id}` — Get a specific trace by ID - `GET /api/v1/cadreen/intelligence/{id}/handoff` — Compact human-review packet: what was asked, what the system knew/didn't know, what happened, suggested actions - `POST /api/v1/cadreen/intelligence/{id}/replay` — Replay trace against current (`mode: "current"`) or historical (`mode: "historical"`) system state - `POST /api/v1/cadreen/intelligence/{id}/promote` — Promote trace into a durable artifact. `kind: "healing"` (recovery precedent), `kind: "memory"` (knowledge item), `kind: "procedure"` (reusable procedure). Direct creation — bypasses purgatory. ### Executions - `POST /api/v1/cadreen/executions` — Start an execution - `GET /api/v1/cadreen/executions` — List executions: `{executions, total, has_more}` - `GET /api/v1/cadreen/executions/{id}` — Get execution status: `{execution_id, operation_id, status, intent, created_at, updated_at}` - `GET /api/v1/cadreen/executions/{id}/stream` — SSE stream of execution events ### Blueprints (repeatable templates) - `POST /api/v1/cadreen/blueprints` — Create a blueprint from an execution, trace, or manual definition - `GET /api/v1/cadreen/blueprints` — List all blueprints - `GET /api/v1/cadreen/blueprints/{id}` — Get blueprint by ID - `PATCH /api/v1/cadreen/blueprints/{id}` — Update blueprint fields - `DELETE /api/v1/cadreen/blueprints/{id}` — Delete a blueprint - `POST /api/v1/cadreen/blueprints/{id}/runs` — Trigger a blueprint run - `GET /api/v1/cadreen/blueprints/{id}/runs` — List runs for a blueprint ### Schedules (time-based executions) - `POST /api/v1/cadreen/schedules` — Create a schedule linked to a blueprint. `trigger`: recurring or cron. - `GET /api/v1/cadreen/schedules` — List all schedules - `GET /api/v1/cadreen/schedules/{id}` — Get schedule by ID - `PATCH /api/v1/cadreen/schedules/{id}` — Update schedule (trigger, params, etc.) - `DELETE /api/v1/cadreen/schedules/{id}` — Delete a schedule - `POST /api/v1/cadreen/schedules/{id}/pause` — Pause a schedule - `POST /api/v1/cadreen/schedules/{id}/resume` — Resume a paused schedule - `POST /api/v1/cadreen/schedules/{id}/runs` — Trigger an immediate run - `GET /api/v1/cadreen/schedules/{id}/runs` — List past runs for a schedule ### Setup (workspace configuration) - `POST /api/v1/cadreen/setup` — One-call workspace configuration. Accepts `connections`, `credentials`, `memory`, and `policies` arrays. Returns consolidated results. A single workspace handles multiple use cases — the intent system routes to the right resources at runtime. Policies are saved as drafts, never active: each comes back with `status: "draft"` and a `reading` of what it would do, and a person turns it on with `POST /api/v1/cadreen/policies/{id}/confirm`. Supports `dry_run: true` to preview without persisting (returns `status: "would_create"`, and `would_draft` for policies). ### Setup Sessions (stateful onboarding) Stateful alternative to POST /setup. Accumulate resources incrementally, then apply them in one call (items are created one at a time; one that fails does not undo the ones before it). - `POST /api/v1/cadreen/setup/sessions` — Create a session (draft status) - `GET /api/v1/cadreen/setup/sessions` — List sessions (up to 20) - `GET /api/v1/cadreen/setup/sessions/{id}` — Get session details - `POST /api/v1/cadreen/setup/sessions/{id}` — Add resources (connections, credentials, memory, policies) - `POST /api/v1/cadreen/setup/sessions/{id}/apply` — Apply the session (requires confirm: true) Lifecycle: draft → applying → applied (or failed) ### Agents CRUD for agents. Agents are living entities that connect tools, follow rules, and work on your behalf. - `POST /api/v1/cadreen/agents` — Create agent (`{ name, purpose? }`) - `POST /api/v1/cadreen/agents/bulk` — Create multiple agents at once (`{ agents: [{ name, purpose? }] }`) - `GET /api/v1/cadreen/agents` — List the agents in your workspace - `GET /api/v1/cadreen/agents/{id}` — Get agent details - `PATCH /api/v1/cadreen/agents/{id}` — Update agent (`{ name?, purpose? }`) - `DELETE /api/v1/cadreen/agents/{id}` — Archive agent (soft delete) - `GET /api/v1/cadreen/agents/{id}/config` — Full config (connectors, policies, memory, versions) - `POST /api/v1/cadreen/agents/{id}/deploy` — Deploy agent (creates immutable version) - `GET /api/v1/cadreen/agents/primary` — Get your workspace's primary agent (the one marked primary, or its oldest) ### Agent Messaging Send messages between agents. The receiving agent's model processes the message. - `POST /api/v1/cadreen/agents/{id}/send` — Send message to agent (`{ content, executionId? }`) - `GET /api/v1/cadreen/agents/{id}/messages` — List message history ### Agent Executions Delegate tasks to specific agents. Each execution uses the agent's config. - `POST /api/v1/cadreen/agents/{id}/executions` — Create execution for agent (`{ task, intentId? }`) - `GET /api/v1/cadreen/agents/{id}/executions` — List executions ### Agent Knowledge What an agent has learned. Facts, rules, patterns — stored permanently, shared with other agents. - `POST /api/v1/cadreen/agents/{id}/knowledge` — Write knowledge (`{ factType, subject, predicate?, object?, source?, confidence?, tags? }`) - `GET /api/v1/cadreen/agents/{id}/knowledge` — List knowledge (workspace-scoped) - `POST /api/v1/cadreen/agents/{id}/knowledge/search` — Semantic search (`{ query, limit? }`) - `DELETE /api/v1/cadreen/agents/{id}/knowledge/{kid}` — Delete knowledge ### Agent Governance Rules that run before every action. Permissive default — audit everything, block nothing. - `GET /api/v1/cadreen/agents/{id}/governance` — List governance policies - `POST /api/v1/cadreen/agents/{id}/governance` — Create policy (`{ name, scope, rules, agentId? }`) - `PATCH /api/v1/cadreen/agents/{id}/governance/{pid}` — Update policy - `DELETE /api/v1/cadreen/agents/{id}/governance/{pid}` — Delete policy - `GET /api/v1/cadreen/agents/{id}/audit` — List audit log entries ### Agent Negotiations When two agents disagree, they work it out. Max 3 rounds. If they can't agree, they ask you. - `POST /api/v1/cadreen/agents/{id}/negotiate` — Start negotiation (`{ toAgentId, proposal, deadlineSeconds? }`) - `GET /api/v1/cadreen/agents/{id}/negotiations` — List negotiations - `GET /api/v1/cadreen/agents/{id}/negotiations/{nid}` — Get negotiation details - `POST /api/v1/cadreen/agents/{id}/negotiations/{nid}/respond` — Respond (`{ action: accept|reject|counter, counterProposal?, reason? }`) ### Agent Events (SSE) Subscribe to agent events as they happen: - `GET /api/v1/cadreen/agents/{id}/stream` — SSE stream. Events: `agent_knowledge_created`, `agent_knowledge_deleted`, `agent_governance_changed`, `agent_message_received`, `negotiation_started`, `negotiation_round`, `negotiation_timeout` ### Federation Cross-workspace agent collaboration with explicit consent. No data crosses without approval. - `POST /api/v1/cadreen/federation` — Create federation link (`{ targetWorkspaceId, permissions? }`) - `GET /api/v1/cadreen/federation` — List federation links - `GET /api/v1/cadreen/federation/{id}` — Get federation link + agent links - `POST /api/v1/cadreen/federation/{id}/approve` — Approve pending federation - `POST /api/v1/cadreen/federation/{id}/suspend` — Suspend active federation (`{ reason? }`) - `POST /api/v1/cadreen/federation/{id}/revoke` — Revoke federation permanently (`{ reason? }`) - `GET /api/v1/cadreen/federation/{id}/permissions` — Get what's shared - `PUT /api/v1/cadreen/federation/{id}/permissions` — Update what's shared - `POST /api/v1/cadreen/federation/{id}/agents` — Link local agent to remote agent (`{ localAgentId, remoteAgentId }`) - `GET /api/v1/cadreen/federation/{id}/agents` — List agent-to-agent links - `DELETE /api/v1/cadreen/federation/{id}/agents/{aid}` — Remove agent-to-agent link ### Federation Events (SSE) Subscribe to federation events as they happen: - `GET /api/v1/cadreen/federation/stream` — Workspace-wide SSE stream. Events: `federation_status_changed`, `federation_consent_requested` - `GET /api/v1/cadreen/federation/{id}/stream` — Per-link SSE stream. Events: `federation_status_changed` ### External Agents (A2A Protocol) Connect to external agents from other systems (LangChain, CrewAI, AutoGPT, etc.) via the A2A protocol. **Discovery:** - `GET /a2a/agents/{id}/.well-known/agent-card.json` — Get an agent's A2A Agent Card at its own base path (public, no auth required; `agent.json` there is the older name). The host-level `/.well-known/agent-card.json` names no agent and answers 404 `agent_not_specified`, which says where to look **JSON-RPC Endpoint (connection credential required):** - `POST /a2a/v1?agent_id={id}` — A2A JSON-RPC 2.0 endpoint for one agent. Methods: `SendMessage`, `GetTask`, `ListTasks`, `CancelTask` (1.0) or `message/send`, `tasks/get`, `tasks/list`, `tasks/cancel` (0.3). Name the version in the `A2A-Version` header (`1.0` or `0.3`; none is 0.3). The card gives the endpoint as `/a2a/v1/agents/{id}` - Every call sends the outside agent's own connection credential: `Authorization: Bearer sk_a2a_...`. API keys and sign-in tokens are refused. The connection is read from the credential; the agent in `agent_id` must be the one it was created for. A connection sees only its own tasks. - Revoking or suspending the connection stops its credential working. - A message the agent's rules say needs a person's approval is accepted and held: the task stays `submitted` with `reasonCode` `awaiting_approval`, a workspace admin approves or declines it in Decisions, and it is rejected after 7 days if nobody decides. Nothing is answered or charged until it is approved. **Connection Management:** - `POST /api/v1/cadreen/agents/{id}/external/inbound` — Connect an outside agent (`{ name }`, workspace admins signed in as a person). Returns the connection, its credential (shown only in this response), `agentCardUrl` and `endpoint`. There is no self-sign-up. - `POST /api/v1/cadreen/agents/{id}/external` — Connect external agent (`{ agentCardUrl }`) - `GET /api/v1/cadreen/agents/{id}/external` — List external connections - `GET /api/v1/cadreen/agents/{id}/external/{cid}` — Get connection details - `POST /api/v1/cadreen/agents/{id}/external/{cid}/approve` — Approve pending connection - `POST /api/v1/cadreen/agents/{id}/external/{cid}/suspend` — Suspend connection - `POST /api/v1/cadreen/agents/{id}/external/{cid}/revoke` — Revoke connection permanently - `DELETE /api/v1/cadreen/agents/{id}/external/{cid}` — Delete connection - `GET /api/v1/cadreen/agents/{id}/external/{cid}/interactions` — List interaction history **Settings:** - `GET /api/v1/cadreen/external-agents/settings` — Get external agents enabled/disabled - `PUT /api/v1/cadreen/external-agents/settings` — Enable/disable external agents (`{ enabled }`) **Default:** External agents are disabled. Enable via settings before connecting. ### Setup Sessions (conversational builder) Stateful conversational builder for creating agent architectures. Guests can use without signing up. - `POST /api/v1/cadreen/setup/sessions` — Create session, streams `agent_proposal` events via SSE - `POST /api/v1/cadreen/setup/sessions/{id}/message` — Follow-up message, streams updates - `POST /api/v1/cadreen/setup/sessions/{id}/reject` — Remove a proposed node SSE events: `processing_status`, `agent_proposal`, `clarifying_question`, `session_complete` ## Route Aliases | Canonical | Alias | Use case | |-----------|-------|----------| | `/api/v1/cadreen/*` | `/cadreen/*` | Shorthand | | `/api/v1/cadreen/intent` | — | Canonical intent surface | | `/api/v1/cadreen/chat/completions` | — | OpenAI-compatible chat completions (supports `stream: true`, governed hybrid tool calling, persistent conversations, cross-conversation memory, self-healing) | | `/v1/tools` | — | List the tools your workspace can use as OpenAI function definitions: each with its id (a routing service's tool_id) and its own input schema as parameters | | `/api/v1/cadreen/executions` | — | Create and manage executions | | `/api/v1/cadreen/executions/{id}` | — | Get execution status | | `/api/v1/cadreen/executions/{id}/stream` | — | SSE stream of execution events | | `/api/v1/maas/workspaces/*` | — | Workspace and key management | ## Chat Completions — Tool Call Governance When `/api/v1/cadreen/chat/completions` is called with `tools`, governance evaluates each proposed tool call: - **Auto-approved:** Returned as standard OpenAI tool calls for client execution - **Held by governance:** Not returned as a tool call. The call is held for a person; the text says so and `pending_actions` lists it: ```json { "choices": [{ "index": 0, "message": { "role": "assistant", "content": "This action needs a person's approval before it can run:\n- process_refund (decision governance_task:7c1e2a40-...)\n\nA person in this workspace has to approve or decline it, signed in. Replying here does not approve it." }, "finish_reason": "stop" }], "operation_id": null, "pending_actions": [{"tool": "process_refund", "tool_call_id": "call_abc123", "operation_id": "5b0d...", "decision_id": "governance_task:7c1e2a40-...", "kind": "governance_task", "source_id": "7c1e2a40-...", "resolve_path": "/api/v1/cadreen/decisions/governance_task:7c1e2a40-.../resolve", "runs_in": "cadreen"}] } ``` **Approval:** A chat reply never approves anything. A signed-in member of the workspace resolves the decision (dashboard inbox, or `POST /api/v1/cadreen/decisions/{id}/resolve` with an `Idempotency-Key`); API keys get `403 person_required`. Approving runs the call once with the proposed arguments. A held tool of your app (`"runs_in": "your_app"`) gets a one-time permit at `permit_path` instead. Calls that could not be held are in `refused_actions` with a `code`. **Hybrid execution:** Cadreen tools execute server-side; client tools returned for client execution. Self-healing retries on failure. **Response latency:** Simple messages (e.g., greetings) return in ~1s. Messages requiring tool use or complex reasoning take 2-5s. When models produce reasoning (chain-of-thought), it is available separately: streaming responses emit `reasoning_delta` events, and non-streaming responses include a `reasoning` field in the message object. ## Responses API OpenAI Responses API compatible endpoint. Supports tool calling, streaming, and conversation state. **Note:** The `model` field is required by the API format but ignored — Cadreen decides which model to use. You can pass any value (e.g., `"cadreen"`). ``` POST /api/v1/cadreen/responses Content-Type: application/json Authorization: Bearer sk_cadreen_... { "model": "cadreen", "input": "What tools do I have?", "instructions": "You are a helpful assistant.", "stream": false } ``` **Key differences from Chat Completions:** - `input` instead of `messages` (accepts string or array of items) - `instructions` instead of system message - `output` array with typed items instead of `choices` - `output_text` helper for quick access to text - `previous_response_id` for conversation state (server-managed) - Tool calls execute server-side (governance, self-healing included) **Streaming:** ``` POST /api/v1/cadreen/responses { "model": "cadreen", "input": "What tools do I have?", "stream": true } ``` Events: `response.created`, `response.in_progress`, `response.output_item.added`, `response.content_part.added`, `response.output_text.delta`, `response.output_text.done`, `response.content_part.done`, `response.output_item.done`, `response.completed` **Conversation state:** ``` // First request POST /api/v1/cadreen/responses { "model": "cadreen", "input": "What is the capital of France?", "store": true } // Second request (references first) POST /api/v1/cadreen/responses { "model": "cadreen", "input": "And its population?", "previous_response_id": "resp_..." } ``` **Tool calling:** ``` POST /api/v1/cadreen/responses { "model": "cadreen", "input": "Refund order 12345", "tools": [ { "type": "function", "name": "process_refund", "description": "Process a refund for an order", "parameters": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] } } ] } ``` Governance evaluates each tool call. Auto-approved tools execute server-side. Blocked tools return a confirmation prompt. ## Cross-Conversation Memory By default, `/api/v1/cadreen/chat/completions` enriches every request with memory from past conversations: - **Episodic memory:** Vector search over past conversations (similar queries, outcomes, learnings) - **Semantic memory:** Knowledge items extracted from prior sessions (vector similarity ≥ 0.4) Memory is per-tenant (workspace). The context is injected into the model prompt as a `[Memory context]` prefix — invisible to the client. **Disable:** Set `"memory": false` in the request body. No embedding call, no vector search. **Header:** `X-Cadreen-Memory: on` or `off` means the same as `"memory"`, for clients that can set headers but not body fields. Header and body must agree, or the request is refused (400 `invalid_memory`). **Per conversation:** `PUT /api/v1/cadreen/conversations/{id}/memory` (API key; a key that lists permissions needs write) with `{"enabled": false}` (`true` turns it on, `null` returns it to the default). The request's field or header wins over it for that request. **Not by message:** saying "disable memory" in a message does not switch memory; the message is answered like any other. ## The `intelligence` Object A successful JSON answer from a decision route includes a short `intelligence` block, and any route's answer includes the full one when you add `?include=intelligence` (Pattern 5 says which answers carry none). The full object is a structured trace of how the system reached its decision — not decorative metadata, but the audit trail. Every answer's own fields come first; `intelligence` and `trace_id` follow them. | Field | Type | Description | |-------|------|-------------| | `intelligence.version` | string | Schema version (currently `"1.0"`) | | `intelligence.summary` | string | One-sentence summary of the trace | | `intelligence.capability.total_available` | int | Total capabilities available | | `intelligence.capability.healthy_count` | int | How many are healthy | | `intelligence.reasoning.capability_matches` | int | How many capabilities matched the request | | `intelligence.memory.healthy` | bool | Whether the knowledge store is reachable | | `intelligence.memory.knowledge_queried` | int | How many knowledge atoms the caller can read. Absent when there are none, and when they could not be counted (`knowledge_unreadable`) | | `intelligence.memory.knowledge_unreadable` | bool | True when the count could not be read (the store did not answer in time, or failed): `knowledge_queried` is absent then, and is not 0. `healthy` is false only when the store could not be reached | | `intelligence.governance.active` | bool | Whether a policy bundle is active | | `intelligence.governance.decision` | string | What the rules in force say (not a decision about this request): `approved`, `conditional` or `blocked`. The intent route writes its own object, whose decision is the decision engine's about the request: `auto`, `handoff` or `clarify_requester` | | `intelligence.governance.unavailable` | bool | True when the rules could not be read, so `active` being false does not mean there are none | | `intelligence.humility.gaps_detected` | int | Total capability gaps found | | `intelligence.humility.blocking` | int | How many of those are blocking (an unhealthy capability). Routes that run the intelligence layer carry it; an intent answer does not | | `intelligence.stages` | array | Ordered list of stages executed | | `intelligence.process.duration_ms` | int | Total processing time in milliseconds | | `intelligence.next_action.type` | string | Action category: `connect_tool`, `add_policy`, `add_memory`, etc. | | `intelligence.next_action.endpoint` | string | The API endpoint to call to resolve the issue | Field stability (the response no longer carries a `field_stability` object): **stable**, safe to branch on: `summary`, `trace_id`, `capability.total_available`, `capability.healthy_count`, `memory.healthy`, `governance.active`, `governance.decision`, `governance.reason_code`, `governance.policy_id`, `governance.policy_name`, `governance.rationale`, `humility.blocking`, `next_action.type`, `next_action.endpoint`. **Evolving**: `capability.active_integrations`, `reasoning.*` counts, `memory.knowledge_queried`, `memory.knowledge_unreadable`, `governance.unavailable`, `stages`. **Internal**, do not depend on: `process.started_at`, `process.duration_ms`, `version`. ## Pagination List endpoints use offset pagination: - Query params: `?limit=50&offset=50` - Response: `{"pagination": {"limit": 50, "offset": 50, "has_more": true}}` - Default page size: 50. Maximum: 200 on most lists (each endpoint's `limit` parameter says) ## Streaming (SSE) Execution progress, narration, and governance events are delivered via Server-Sent Events: ``` GET /api/v1/cadreen/executions/{id}/stream Accept: text/event-stream → Events: stage_start, tool_call, governance_check, progress, execution_complete → Resume after disconnect using Last-Event-ID header → The server closes open streams when it restarts (a deploy): reconnect with Last-Event-ID to pick up the events you missed → Conversation and connection-session streams resume the same way. Notifications, agents, federation, training-job, MCP, A2A, mission-analysis and platform-event streams do not resume: a reconnect gets what the stream sends when it opens, then what happens next, not the events you missed → A stream asked for while the server is stopping is refused or closed with no answer: try again in a moment → Reconnect with the same execution ID — execution continues server-side ``` A streamed chat completion or intent (`"stream": true`) sends a `: keepalive` comment every 15 s while it waits, so a proxy never closes it; SSE parsers skip comments. If Cadreen takes more than 10 s to decide what to do, the stream opens before it has decided: the operation ID is then in the first data chunk's `operation_id`, not in the `X-Cadreen-Operation-Id` header, and a refusal arrives as an error event instead of a status. If it hasn't decided after 2 minutes, it stops, and the stream ends with a `timeout` error event (chat: `data: {"error": {"code": "timeout", ...}}` then `data: [DONE]`; intent: `data: {"type": "error", "error": {"code": "timeout", ...}}`). Nothing was started, so sending it again with the same `Idempotency-Key` runs it. ### Agent Events Stream an agent's events over Server-Sent Events (knowledge changes, governance updates, messages, negotiations): ``` GET /api/v1/cadreen/agents/{agentID}/stream Accept: text/event-stream → Events: agent_knowledge_created, agent_knowledge_deleted, agent_governance_changed, agent_message_received, negotiation_started, negotiation_round, negotiation_timeout ``` ### Federation Events Subscribe to workspace-wide federation events: ``` GET /api/v1/cadreen/federation/stream Accept: text/event-stream → Events: federation_status_changed, federation_consent_requested ``` For a specific federation link: ``` GET /api/v1/cadreen/federation/{federationID}/stream Accept: text/event-stream → Events: federation_status_changed ``` ## Limits & Constraints - **Workspace isolation:** Data never crosses workspace boundaries. Each API key is scoped to one workspace. - **Output modes:** `clarify` (asks questions before proceeding), `direct` (answers immediately), `execution` (runs a multi-step task with tools). - **Capability honesty:** When a tool is missing the answer says so: status `blocked_by_missing_connection`, with `next_action.endpoint` to connect it. It will not pretend to have tool access. - **Governance enforcement:** Policies are architectural, not advisory. Blocked actions cannot be bypassed by prompt engineering. - **Human-in-the-loop:** Critical decisions require human approval. The system is designed to ask for help, not to act alone. - **Structural improvement:** The system gets smarter the more you use it. It remembers what worked, automates repeated tasks, and suggests improvements — all requiring your approval before changes take effect. The model itself is one instrument in the orchestra. - **Idempotency:** Send an `Idempotency-Key` header (a client-generated UUID) with a POST to the Cadreen API. The same key with the same body within 24 hours answers with the first response, and nothing runs again. The same key with a different body is `409 idempotency_conflict`: never retry it, use a new key. A repeat while the first is still running is `409 idempotency_in_progress` with `Retry-After`: retry with the same key. A streamed answer (server-sent events, a streamed chat reply) goes out as it is written and is never kept whole. A repeat of a streamed request, or of one whose answer couldn't be kept, is `409 idempotency_response_not_kept`, and nothing runs: the first request may or may not have started work, so check its operation, or send it again with a new key. If the key can't be checked, the answer is `503 idempotency_unavailable` and nothing runs. A route whose answer carries a secret shown once (`POST /webhooks`, `POST /edge/devices`, `POST /quickstart` and others) never keeps it: a repeat makes a new one, and the response says so in `Idempotency-Key-Ignored`. `GET` endpoints are always idempotent. `POST /setup` additionally has resource-level idempotency: credentials are upserted, memory items with the same content are skipped, and policy bundles with the same name are skipped. Example: ``` POST /api/v1/cadreen/intent Authorization: Bearer sk_cadreen_... Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890 {"messages":[{"role":"user","content":"Refund order 456"}]} → First call: 200 with result → Retry with same Idempotency-Key: 200 with the SAME result (not a second refund) ``` - **Pricing:** Free while we launch: nothing is charged, each workspace can use Cadreen up to a daily limit (counted at the prices below), and adding money is paused. Relay costs nothing. Once charges start, there are two ways to pay — per token or per outcome. Both give you the full Cadreen engine. Per-token pricing: $2.99/M output, $0.99/M input, $0.29/M cached input tokens. Per-outcome pricing: work out per-use-case pricing before you go live. Cadreen acts as your model — you don't manage provider keys. See the pricing page for details. ## The Golden Path (3-Step Integration Ritual) The canonical integration sequence every developer should follow. Skip orchestration-by-discovery — start here: ### Step 1 — Install SDK & create client ```ts import { Cadreen } from '@cadreen/sdk'; const cadreen = new Cadreen({ apiKey: process.env.CADREEN_API_KEY }); ``` Python: `pip install cadreen-sdk` then `from cadreen import Cadreen; cadreen = Cadreen(api_key="sk_...")` Go: `go get github.com/timothy-billingrails/cadreen-sdks/go/cadreen@latest` then `client := cadreen.NewClient(cadreen.CadreenConfig{APIKey: "sk_..."})` ### Step 2 — Setup from purpose (preview, then apply) Describe what you are building. Cadreen returns proposals — connections, memory, policies — for your review. Use `dry_run: true` to preview without persisting: ``` POST /api/v1/cadreen/setup {"purpose": "Resolve ecommerce support requests with refund policy enforcement", "dry_run": true} ``` // Returns "dry_run": true and "proposals" (type, description, detail). Nothing is saved. Review them, then apply: ``` POST /api/v1/cadreen/setup {"memory": [{"type": "reference", "content": {"text": "..."}}, ...], "policies": [{"name": "...", "rule": "..."}, ...]} ``` // Proposals from a purpose are never applied by themselves: send the items you accept, as above. ### Step 3 — Invoke intent & switch on typed outcome ```ts // The fields below are the ones of the answer the server sends. A block is a 403 whose body is the answer, not an error. const result = await cadreen.intent.invoke({ input: 'Customer 123 wants a refund for order 456', user: { id: 'agent_7' }, }); switch (result.type) { case 'direct': return result.message.content; case 'clarify': return renderQuestions(result.clarification.questions); case 'execution': return stream(result.execution.stream_url); case 'blocked': return renderBlock(result.meta.governance); // { decision, reason } case 'connect_required': return connectTool(result.next_action.endpoint); } ``` The golden path is: `create workspace → setup (review proposals) → apply resources → intent.invoke → type switch → inspect trace`. Create a workspace first (via JWT auth, as an owner or admin of your organization's default workspace; signing in the first time already made one): ``` POST /api/v1/maas/workspaces {"name": "My App", "slug": "my-app"} ``` Then create an API key for it and use `/setup` with that key. For JWT auth with no workspaces, `/setup` auto-creates a "default" workspace. ## Canonical Outcome Types Every intent call returns exactly one of five discriminated outcomes. **Branch on `status` (the table in Pattern 5), never parse `content` prose. `type` groups the same answer by the object that holds it:** | `type` | Meaning | Contains | |----------|---------|----------| | `direct` | Answered immediately, no action needed | `message`: `{ role, content }` | | `clarify` | More information required before proceeding | `clarification`: `{ questions, conversation_id }` | | `execution` | Multi-step execution is running | `execution`: `{ id, status, stream_url, poll_url }` | | `blocked` | Governance gate prevented action (a 403 whose body is the answer) | `meta.governance`: `{ decision, reason }`; the rule is in `intelligence.governance` when the answer has one | | `connect_required` | Required tool or integration is missing (also a 403) | `meta.governance` as above, and `next_action.endpoint` to connect | `status` is on every answer and has eight values (Pattern 5), more granular than `type`. Read `type` only to know which object carries the answer. A 200 answer of a route that runs the intelligence layer includes `trace_id` (top-level), whatever the profile, unless it is a stream, an array, a CSV or a file download. A block is a 403: its body has no `trace_id`, and its trace id is in the `X-Cadreen-Trace-ID` header, which those routes set on their JSON object answers, the errors their handlers write and their streams (a refusal before the handler runs, which is sign-in, the API key check, a rate limit, a Content-Type the server refuses with 415 or an idempotency conflict, an array answer, a CSV export and a file download carry none). Use it for debugging and to quote to support: no trace is stored for a block or an error, so `GET /intelligence/{id}` answers 404 for it. ## Gate Decisions (Machine-Readable) What the rules say about a request is in the `intelligence.governance` object of the response — never parse the reason from prose: ```json { "intelligence": { "governance": { "active": true, "decision": "blocked", "reason_code": "policy_requires_approval", "policy_id": "pol_refund_threshold", "policy_name": "Refund threshold", "rationale": "Refunds above $500 require manager approval", "next_actions": [ {"type": "create_escalation", "label": "Request human approval", "endpoint": "/api/v1/cadreen/escalations", "reason": "Policy pol_refund_threshold requires human approval"}, {"type": "revise_intent", "label": "Adjust the action to satisfy policy", "reason": "Modify the action to comply with the policy constraint"} ] } } } ``` `reason_code` is one of a closed set of governance reason codes, written with underscores (the enum of `reason_code` in the OpenAPI spec lists them all), and it is left out when the server holds none of them. `unavailable: true` means the rules could not be read, so the request was not checked against them: it is not the same as there being none. The `X-Cadreen-Gate` response header (`ok`, `blocked`, `escalate`, `needs_input`) is sent on a successful answer, with `profile="audit"` and with an `intelligence` object a route writes itself (a short block built from the route's answer sends none: no gap analysis ran) (the intent route; chat completions too, where it is always `ok`: the object it writes never sets either field the header is read from). An error never has it: a block is a 403. An answer with no block, and the full object, have none, and neither has an answer whose rules could not be read (`intelligence.governance.unavailable` is true: nothing says what the gate is), unless a blocking gap makes it `blocked`. Read `intelligence.governance` and `intelligence.humility` in the body, and do not take a missing header for `ok`. ## Webhook Events | Event | Emitted when | Payload key fields (`data`) | |-------|-------------|-------------------| | `decision.created` | A decision waits for a person in a workspace | `id`, `kind`, `source_id`, `workspace_id`, `workspace_name`, `status`, `title`, `summary`, `question`, `what_happened`, `if_nobody_decides`, `what_clears_it` (a needs-a-look item), `required_role`, `moves_money`, `money_unknown`, `called_as`, `actions` (`action`, `label`, `description`), `linked_operation_id`, `waiting_since`, `due_at` (when it has one), `url` (never the held call's own values) | | `decision.resolved` | That decision no longer waits | the same, plus `resolution` (`outcome`: `decided` or `closed`; `action`, `action_label`, `decided_by` (`user_id`, `name`), `decided_at` when a person chose; `closed_at`) | | `decision.overdue` | A decision with a due time is still waiting after it (once per decision; the people who can decide it are emailed too) | the same as `decision.created`; `due_at` is always there and `status` is `pending` | | `alert.triggered` | A device reading crossed an alert rule, or a routing provider became unhealthy | device: `workspace_id`, `event_id`, `device_id`, `sensor_id`, `value`, `threshold`, `severity`, `message`, `fired_at`; routing: `workspace_id` (absent for a company-wide group), `level`, `entity_type`, `entity_id`, `entity_name`, `group_id`, `message` | | `alert.acknowledged` | A person acknowledged a device alert | `workspace_id`, `event_id` | | `alert.resolved` | A device reading came back across the alert rule's value: the alert is over (once per alert) | `workspace_id`, `event_id`, `device_id`, `sensor_id`, `value`, `threshold`, `severity`, `summary`, `alert_summary`, `fired_at`, `resolved_at` | | `schedule.run.created`, `schedule.run.started`, `schedule.run.completed`, `schedule.run.failed`, `schedule.run.needs_human`, `schedule.run.retry_scheduled` | A scheduled run was made, started, finished, failed for good, is waiting for a person, or will be tried again | `schedule_id`, `schedule_name`, `blueprint_id`, `run_id`, `mission_id` (when it has one), `status`, `attempt`, `max_attempts`, `scheduled_for`, `retry_at` (when a retry is set), `error` (`code`, `message`, `category`, `recoverable`; when it has one) | | `schedule.paused`, `schedule.resumed` | A schedule was paused (by a person, or by the scheduler after repeated failures) or resumed | `schedule_id`, `schedule_name`, `blueprint_id`, `status` (`paused`, `error` when the scheduler stopped it, `active` when resumed), `paused_by`, `reason`, `next_run_at` (no `workspace_id`) | | `operation.failed`, `operation.waiting_for_human` | Any job moved to failed, or to waiting for a person | `operation_id`, `workspace_id`, `trigger`, `from_state`, `to_state`, `cause` (when there is one), `occurred_at` | | `execution.completed`, `execution.failed` | An execution ended: a job started by a request completed or failed | the same as `operation.failed` | | `router.failover` | Router moved a run's work to a backup after a service refused it | `event_id`, `workspace_id`, `execution_id`, `operation_id`, `group_id`, `group`, `from_provider`, `to_provider`, `reason` (the failure category), `run_status` | | `router.provider_unavailable` | A service started resting (it kept failing, or asked Router to wait), or Cadreen can't sign in to it | `event_id`, `workspace_id`, `group_id`, `provider_id`, `provider`, `reason` (`failing`, `rate_limited`, `auth_expired`), `resting_until` | | `router.rule_invalid` | A saved rule no longer reads, so its job routes nothing | `event_id`, `workspace_id`, `group_id`, `rule_id`, `invalid_reason` | | `router.run.succeeded` | A Router run finished and the service did the work | `event_id`, `workspace_id`, `operation_id`, `execution_id`, `group_id`, `group`, `provider`, `state`, `test_run` (true for a test run, which went only to test services), `decided_by` (`router` or `person`) | | `router.run.failed` | A Router run failed, or was refused before anything was sent | the same, plus `failure_category`, `explanation_code`, `nothing_sent` | | `router.run.needs_check` | Nobody knows whether the service did the work: a person must check | the same as `router.run.failed` | | `router.run.waiting` | A Router run waits for the service's result | the same as `router.run.succeeded` | | `router.run.needs_person` | Cadreen looked at the service itself and stopped looking: a person has to decide | `event_id`, `workspace_id`, `operation_id`, `execution_id`, `group_id`, `group`, `provider`, `cause` (a closed list: no_key_kept, not_declared, tool_not_read_only, governance_refused, credentials_missing, lookup_failed, answer_unreadable, several_found, none_found, time_limit, check_limit, wait_expired), `test_run` | Also sent: `operation.failed` and `operation.waiting_for_human` when any job moves to failed, or to waiting for a person (`data`: `operation_id`, `workspace_id`, `trigger`, `from_state`, `to_state`, `cause`, `occurred_at`), and `execution.completed` and `execution.failed` when an execution ends (the same data). Deprecated, never sent, and to be removed: `clarification.required`, `escalation.created`, `escalation.resolved` (use `decision.created` and `decision.resolved`), `connector.auth_expired`, `policy.blocked`, `healing.succeeded`, `healing.failed` and `trace.persisted`. A subscription that lists one is still created, and the answer lists them in `ignored_events` with a sentence. The same failed and waiting jobs are emailed too (an execution's end, `execution.*`, is a webhook only): to the person who started one unless they turned it off, and to the workspace's admins and owners unless an admin turned the copy off (a job started with an API key has no person: admins only, naming the key's name). A person gets one email each time a job fails or waits for a person; if they were already emailed about the workspace in the last 15 minutes, the next ones come together as one digest. A job waiting for a person that already has a decision in the inbox isn't emailed: the decision's email asks them. `GET`/`PUT /api/v1/cadreen/notifications/job-emails` reads and sets your own switches (`job_failed_email`, `job_waiting_email`; both default on and apply in every workspace of the account) and the workspace's `admins_copied` (default on; only a workspace admin or owner may change it: otherwise 403 `workspace_admin_required`, nothing saved; the answer's `can_change_admins_copied` says whether you may). A person only: an API key gets 403 `person_required`; a non-member gets 403 `workspace_membership_required`. Every event goes to each active subscription of the workspace it happened in that lists it, never to another workspace's, even one of the same account (a `schedule.*` event has no `workspace_id`: keep the workspace with the subscription). The body is `{"event", "timestamp", "trace_id", "data"}`. Headers: `X-Cadreen-Webhook-Signature`, `X-Cadreen-Webhook-Event` (the event name) and `X-Cadreen-Webhook-ID` (the delivery ID). Answer with any 2xx. A delivery that fails or answers anything else is tried up to 5 times, after 4, 9, 16 and 25 seconds. Those retries are held in the server's memory, so one still waiting when the server restarts is lost and nothing sends it again. A retry carries the same body and signature as the first try, so `t` is the first attempt's time: allow a few minutes when you check its age, not a few seconds. Delivery is at least once: the same event can reach you twice, for instance when a hand-over failed and was retried. There is no delivery-level idempotency key (`X-Cadreen-Webhook-ID` names one delivery, and a repeat is a new delivery), so dedupe. A decision event: on `data.id` plus the event name from `X-Cadreen-Webhook-Event`. An `operation.*` or `execution.*` event: on `data.operation_id`, the event name and `data.occurred_at`, because a job can fail or wait for a person more than once and each time is its own event (`occurred_at` is the same on every repeat of one). A `schedule.run.*` event: on `data.run_id`, the event name and `data.attempt` (a retry is a new run with its own `run_id` and an `attempt` one higher; the event doesn't say which run it retries). `schedule.paused` and `schedule.resumed` carry no id or time of their own occurrence in `data` (`data` has `schedule_id`, `schedule_name`, `blueprint_id`, `status`, `paused_by`, `reason` and `next_run_at`), and retries can deliver an older pause after a later resume, so don't take the last one to arrive as the state. Order them by the envelope's `timestamp`, which is set once when the event is made and is the same on every retry, or read the schedule's current `status` from `GET /api/v1/cadreen/schedules/{id}`. Where there is an `event_id`, use it. Every event for one decision carries the same `trace_id`, `decision:` followed by `data.id`; that ties them together but does not tell the events apart, so keep the rule above. A Router event can arrive twice: drop an `event_id` you have seen. A failover goes to the workspace whose run it was, also for a company-wide job; a company-wide job's other Router events go to no workspace's webhooks (closed with `not_sent_reason` `no_workspace`). `GET /api/v1/cadreen/routing/events` lists every Router event and when it was handed to the webhooks; one the webhook store couldn't take is tried again. `X-Cadreen-Webhook-Signature` is `t=,sha256=`: an HMAC-SHA256, keyed with the subscription's secret, of `.`. Verify it before processing, and refuse an old `t`. ```ts import crypto from "crypto"; function verifySig(rawBody: string, header: string, secret: string): boolean { const parts: Record = {}; for (const kv of header.split(",")) { const i = kv.indexOf("="); if (i > 0) parts[kv.slice(0, i)] = kv.slice(i + 1); } if (!parts.t || !parts.sha256) return false; if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody, "utf-8").digest(); const got = Buffer.from(parts.sha256, "hex"); return got.length === expected.length && crypto.timingSafeEqual(got, expected); } // rawBody: the raw request body string (do NOT JSON.parse first) // header: req.headers["x-cadreen-webhook-signature"] // secret: the secret returned (once) when you created the subscription ``` ## Stable Contracts (Fields You Can Depend On) These fields are committed as stable. Code against them: | Field | Meaning | |-------|---------| | `trace_id` | Top-level in a successful answer that is a JSON object, from a route that runs the intelligence layer, in all profiles (lean/audit/full). Not in a stream, an error or a block's body: read the header. | | `type` | The outcome discriminant: `direct`, `clarify`, `execution`, `blocked`, `connect_required` | | `status` | Intent-level status: `answered`, `needs_input`, `ready_to_execute`, `executing`, `planning`, `blocked_by_policy`, `blocked_by_missing_connection`, `failed` | | `intelligence.governance.decision` | What the rules in force say: `approved`, `conditional` or `blocked`; on the intent route's own object, the decision engine's: `auto`, `handoff` or `clarify_requester` | | `intelligence.governance.reason_code` | Machine-readable reason, one of the closed set in the OpenAPI spec (e.g. `policy_requires_approval`). Not prose. | | `next_action.type` | What to do next: one of the thirteen values Pattern 5 lists | | `next_action.endpoint` | The API endpoint to call to resolve the issue | | `error.code` | Stable error code. Enum values documented in the error glossary. | | `X-Cadreen-Gate` | Response header. Values: `ok`, `blocked`, `escalate`, `needs_input`. Sent on a successful answer, with `profile="audit"` and with an `intelligence` object a route writes itself (the intent route; chat completions, where it is always `ok`), and not when the rules could not be read (`intelligence.governance.unavailable`) unless a blocking gap makes it `blocked`; a missing header is not `ok` | | `X-Cadreen-Trace-ID` | Response header. Trace ID for the request: set on the JSON object answers, streams and handler-written errors of the routes that run the intelligence layer, and not on a refusal before the handler runs (sign-in, the API key check, a rate limit, a Content-Type that is not JSON, an idempotency conflict), an array answer, a CSV export or a file download. | **Do not depend on:** `intelligence.process.duration_ms` (varies with load), `intelligence.reasoning.*` internal fields, message `content` strings (use `type` to branch), cursor tokens (opaque), pagination `has_more` semantics by route (read the specific route docs). ## Field Stability Changelog | Date | Change | Fields | |------|--------|--------| | 2026-10-22 | Corrected | The `gate` object the rows below name was never sent. A governance decision is `intelligence.governance` (`decision`, `reason_code`, `policy_id`, `next_actions`), stable as listed above. `reason_code` is one of a closed set, written with underscores (`policy_requires_approval`), never `policy.requires_human_approval` or `capability.missing`. | | 2026-06-09 | Added stable fields | `trace_id` (all profiles), `gate.reason_code`, `gate.policy_id`, `gate.next_actions[]` | | 2026-06-09 | Item type/kind model clarified | `type` (6 user-writable categories), `kind` (29 system-assigned classifications), `classified` (boolean). Endpoint `GET /memory/types` returns `type_values` and `kind_values`. | | 2026-06-09 | Removed phantom webhook events | `intent.created`, `intent.needs_input`, `execution.stage_changed` removed — not in Go source. 17 actual events remain. | | 2026-06-09 | Escalation decision values | Stable values: `auto`, `auto_complete`, `handoff`, `escalate`, `clarify_requester`, `abstain`. Not "approved/rejected". | | 2026-06-09 | Added evolving fields | `intelligence.governance.reason_code`, `intelligence.governance.policy_id`, `intelligence.governance.next_actions` | | 2026-05 | Stable since initial release | `type`, `status`, `gate.status`, `next_action.type`, `next_action.endpoint`, `error.code`, `X-Cadreen-Gate`, `X-Cadreen-Trace-ID` | **Stable** = safe to branch application logic on. **Evolving** = shape may change; depend with caution. **Removed** = will be announced 30 days before removal. Subscribe to the changelog for deprecation notices. ## What Not to Do Common integration mistakes: 1. **Do not parse prose to decide what happened.** Always use `type`, `status`, or `intelligence.governance.reason_code`. 2. **Do not ignore governance gates.** The header says `blocked` — the action did not execute. Do not retry. 3. **Do not retry blindly.** Check `intelligence.governance` and `next_action` first. Retries without resolving the root cause are wasted calls. 4. **Do not store secrets in memory items.** Use `/credentials` for secrets. 5. **Do not treat `intelligence.process.duration_ms` as a business SLA signal.** It varies with model latency and load. 6. **Do not hard-code field paths inside `intelligence`.** Use `field_stability.stable` to know what is safe. 7. **Do not call `/setup` once and discard the result.** The setup response is your installation checklist throughout development. ## For More - Full API reference: https://cadreen.com/docs?section=api-reference - OpenAPI spec (machine-readable): https://cadreen.com/api/v1/cadreen/docs/openapi.json - Markdown docs: https://cadreen.com/docs/md - Error reference: https://cadreen.com/docs/errors - Quickstart: https://cadreen.com/docs?section=quickstart - Contact: support@accomplishanything.today