API Reference
Base URL: http://your-aicontrol-host:8001
Interactive docs (Swagger UI): http://your-aicontrol-host:8001/docs
Authentication
AIControl uses two token types:
Machine API tokens — Issued by POST /tokens or scripts/issue_token.py. Used by AI agents and automation scripts. Stored as SHA-256 hashes; never recoverable after issuance.
Human JWTs — Issued by POST /auth/login after password authentication. Used by dashboard users and the React frontend. 8-hour expiry. Not stored in the database — validated by signature only.
Pass either type as a Bearer header:
Authorization: Bearer <token>Endpoint access by token type:
| Role | Token type | Can call |
|---|---|---|
agent (agent-scoped) | Machine | POST /mcp/{server_id}/tools/list, POST /mcp/{server_id}/call_tool, GET /reviews/{id} |
admin (machine) | Machine | All management endpoints |
admin (human) | Human JWT | Dashboard endpoints (require_human or require_admin) |
Setup
GET /setup/status
Returns whether first-run setup is required. Use this to decide whether to redirect to /setup on the frontend.
Auth: none required
Response 200:
{ "setup_required": true }setup_required: false once any user exists in the database.
POST /setup/complete
Create the root administrator account and organisation on first run. Returns 409 if any user already exists.
Auth: none required
Request:
{
"org_name": "Acme Corp",
"timezone": "America/New_York",
"full_name": "Alice Smith",
"email": "alice@acme.com",
"password": "securepassword"
}timezone must be a valid IANA timezone string (e.g. "UTC", "Europe/London"). password minimum 8 characters.
Response 200:
{
"token": "eyJhbGc...",
"first_login": true
}Auth
POST /auth/login
Authenticate with email and password. Returns a human JWT valid for 8 hours.
Auth: none required
Request:
{
"email": "alice@acme.com",
"password": "securepassword"
}Response 200:
{
"token": "eyJhbGc...",
"user": {
"id": "uuid",
"email": "alice@acme.com",
"name": "Alice Smith",
"role": "admin"
}
}Returns 401 for invalid credentials. Returns 403 for deactivated accounts.
POST /auth/magic-link/validate
Validate a magic link token (used in the invite and password reset flows). Does not issue a JWT — call POST /auth/set-password after this to complete the flow.
Auth: none required
Request:
{ "token": "plaintext_invite_token_from_url" }Response 200:
{
"name": "Bob Jones",
"email": "bob@acme.com"
}Returns 401 for expired or invalid tokens.
POST /auth/set-password
Set password using a valid magic link token. Issues a human JWT on success.
Auth: none required
Request:
{
"token": "plaintext_invite_token_from_url",
"password": "newpassword"
}Response 200:
{
"token": "eyJhbGc...",
"user": { "id": "uuid", "email": "bob@acme.com", "name": "Bob Jones", "role": "viewer" }
}MCP Servers
Admin-only CRUD for registering downstream MCP servers with the gateway. No credential field exists anywhere in this API — registration is metadata-only (Decision 9 of the gateway pivot: pure passthrough, no stored secret).
POST /mcp-servers
Register a downstream MCP server.
Auth: admin
Request:
{
"name": "invoicing-mcp",
"base_url": "https://invoicing.internal/mcp",
"approved_tools": ["get_invoice", "create_invoice"]
}approved_tools is optional, defaults to [] (meaning every tool the
downstream server exposes is reachable through this registration, subject to
policy and the agent's own approved-tools list).
Response 201:
{
"id": "3f2a1c8b-...",
"name": "invoicing-mcp",
"base_url": "https://invoicing.internal/mcp",
"status": "pending_review",
"approved_tools": ["get_invoice", "create_invoice"],
"created_at": "2026-09-09T10:00:00Z"
}New registrations always start in status: "pending_review".
GET /mcp-servers
List all registered MCP servers, newest first.
Auth: admin
Response 200: array of server objects (same shape as the POST response).
GET /mcp-servers/
Get one registered server.
Auth: admin
Response: 200, or 404.
PATCH /mcp-servers/
Update a server's status and/or approved_tools. All fields optional.
Auth: admin
Request:
{ "status": "active" }status must be one of pending_review, active, or blocked — any other
value is rejected. Only active servers accept gateway traffic; a
non-active server's gateway calls return 403.
Response: 200 with updated server, or 404.
DELETE /mcp-servers/
Delete a server registration.
Auth: admin
Response: 204, or 404.
MCP Gateway
The core integration point. Agents point their MCP client at these endpoints
instead of the real downstream server. Both require an agent-scoped
token — an admin token, or an agent-role token issued without an agent_id,
is rejected with 403.
POST /mcp//tools/list
Proxies tools/list to the registered downstream server, filtered to the
server registration's approved_tools if non-empty.
Auth: agent-scoped token
Request:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }Response 200: the downstream server's own tools/list response
(optionally filtered), or a JSON-RPC error if the server is unreachable:
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32000, "message": "Downstream MCP server unreachable"}}Returns 403 if the server's status isn't active, or 404 if
server_id doesn't match a registered server.
POST /mcp//call_tool
The core interception point. Evaluates Cedar policy on the requested tool call, audits the result, and — if allowed — forwards to the real downstream server and scans the response before returning it.
Auth: agent-scoped token
Request:
{
"jsonrpc": "2.0",
"id": 1,
"params": {
"name": "query_credit_bureau",
"arguments": {"applicant_id": "A-10042", "report_type": "full"},
"session_id": "b3e1f9a2-...",
"sequence_number": 3,
"workflow": "loan_application_review",
"input_tokens": 812,
"output_tokens": 143,
"cost_usd": 0.0091
}
}session_id and sequence_number are required — a missing or invalid value
returns a -32602 JSON-RPC error, not an HTTP 422. workflow defaults to
"unassigned". input_tokens/output_tokens/cost_usd are optional and
only matter to token_budget policy conditions scoped to this tool.
Response 200 (allowed, forwarded): the downstream server's own
tools/call response, unchanged.
Response 200 (denied by approved-tools gate, Cedar policy, review, or
response scan): a JSON-RPC result with isError: true — never an HTTP
error code:
{
"jsonrpc": "2.0",
"id": 1,
"result": {"content": [{"type": "text", "text": "Denied by policy: policy_matched:block_dangerous_tools"}], "isError": true}
}Response (malformed request): -32602 JSON-RPC error for missing/invalid
session_id or sequence_number.
Response (downstream unreachable): -32000 JSON-RPC error, same shape as
tools/list.
Returns 403 if the server's status isn't active or if the token isn't
agent-scoped, or 404 if server_id doesn't match a registered server.
Agents
GET /agents
List all registered agents with enriched metadata.
Auth: admin
Response 200: array of agent objects including last_active, deny_rate, approved_at, created_at, system_prompt_hash, plus coverage-tracking fields: framework, hook, sdk_version, workflow, coverage_last_seen_at, coverage_state (unknown | governed | installed_not_firing), silent_noop_warnings (array), unresolved_systems (array). These fields are populated by the adapter's own install-time handshake to POST /agents/{agent_id}/coverage (see below) — the handshake distinguishes "agent never ran" from "agent ran but the hook never fired," which traffic alone can't tell apart. The call goes only to the customer's own self-hosted API; nothing leaves their infrastructure.
GET /agents/
Get one agent.
Auth: admin
Response: 200 with agent object (same coverage fields as GET /agents), or 404.
POST /agents//coverage
Install-time handshake — called by the SDK adapter itself, not by a human. Records which framework/hook/SDK version is instrumenting this agent and which workflow it's reporting.
Auth: agent token (must match agent_id, or be unscoped)
Request:
{
"framework": "openai_agents",
"hook": "on_tool_start",
"sdk_version": "0.4.0",
"workflow": "loan_application_review",
"agent_name": "loan-underwriting-agent",
"silent_noop_warnings": []
}An unrecognized agent_id is auto-registered in observe mode rather than rejected — a live, ungoverned agent nobody registered is exactly what this handshake exists to surface, and 404ing would leave it invisible instead.
Response 200: { "agent_id": "...", "coverage_state": "governed" }
POST /agents
Register a new agent.
Auth: admin
Request:
{
"name": "loan-underwriting-agent",
"owner": "platform-team@company.com",
"framework": "langchain",
"model_version": "gpt-4o",
"approved_tools": ["query_credit_bureau", "read_loan_application"],
"system_prompt_hash": "sha256hex",
"governance_mode": "observe",
"metadata": {}
}All fields except name and owner are optional. governance_mode is "govern" (default —
policy decisions are enforced) or "observe" (decisions are recorded exactly as if enforced,
but the gateway never actually blocks a call for this agent — useful for rolling out a new
policy set risk-free).
Response 201: agent object with generated id.
PUT /agents/
Update an agent. governance_mode can be changed here too ("govern" or "observe").
Auth: admin
Response: 200 with updated agent, or 404.
DELETE /agents/
Delete an agent record.
Auth: admin
Response: 204, or 404.
DELETE /agents//token
Revoke all active tokens for this agent.
Auth: admin
Response 200: { "revoked": 2 }
PATCH /agents//approved-tools
Replace the agent's approved tools list. The gateway enforces this before policy evaluation runs at all — any tool absent from this list is denied immediately with reason: tool_not_approved_for_agent.
Auth: admin
Request: { "approved_tools": ["tool_a", "tool_b"] }
Pass [] to remove all per-tool restrictions for that agent.
Response 200: { "agent_id": "...", "approved_tools": ["tool_a", "tool_b"] }
Tokens
GET /tokens
List all issued API tokens. The raw token value is never returned.
Auth: admin (human JWT)
Query parameters: active_only (boolean, default true)
Response 200: array of { id, role, description, agent_id, agent_name, revoked, created_at }
POST /tokens
Issue a new API token. If agent_id is provided, any prior active token for that agent is revoked automatically.
Auth: admin
Request:
{
"role": "agent",
"description": "loan-underwriting-agent prod token",
"agent_id": "7c4d2e8f-..."
}Response 200:
{
"token_id": "a3b1c9d2-...",
"role": "agent",
"description": "loan-underwriting-agent prod token",
"agent_id": "7c4d2e8f-...",
"token": "eyJhbGc..."
}The token value is shown once. Save it immediately.
Policies
GET /policies
List all policies ordered by priority.
Auth: admin
Response 200: array of policy objects. Scope fields carry a camelCase alias: principalType, principalId, actionTool, resourceSystem, alongside effect, cedar_text, condition, library, priority, category.
GET /policies/library
List all library template policies (library=true). These are inactive templates, not active rules.
Auth: admin
Response 200: array of policy objects grouped by category.
GET /policies/
Get one policy.
Auth: admin
Response: 200, or 404.
POST /policies
Create a policy. Compiles to Cedar and takes effect on the very next call_tool request through the gateway — no restart, no bundle push.
Auth: admin
Request:
{
"name": "block_large_disbursements",
"description": "Block loan disbursements above $10,000",
"principal_type": null,
"principal_id": null,
"action_tool": null,
"resource_system": null,
"effect": "deny",
"condition": {
"tool_name_in": ["initiate_transfer", "disburse_loan_funds"],
"numeric_conditions": { "amount": { "gt": 10000 } }
},
"severity": "critical",
"compliance_frameworks": ["SOC2", "OCC"]
}principal_type/principal_id scope the policy to one agent or agent group (null = every agent); action_tool scopes to one tool (null = any tool); resource_system scopes to one business system (null = any system). effect is deny or review — there is no stored allow. See Policies for the full condition-key vocabulary.
A policy with an empty condition must still constrain at least one of principal_id, action_tool, or resource_system, or the request is rejected with 422 — an unconstrained, unconditional policy would deny every call from every agent.
priority is accepted but not read by the engine — see Policies → Precedence.
Response 201: policy object with generated id, including the compiled cedar_text.
PUT /policies/
Update a policy. All fields optional (same shape as POST /policies). Recompiles cedar_text and takes effect on the next call_tool request through the gateway.
Auth: admin
Response: 200 with updated policy, or 404.
GET /policies//activity
How often this policy has fired recently.
Auth: admin
Query parameters: window (default "7d")
Response 200:
{ "window": "7d", "fired": 12, "calls_evaluated": 340 }calls_evaluated matches on the policy's agent + tool scope only — a business system isn't persisted on the audit event itself (it's resolved at gateway-call time), so for a system-scoped policy this count is an upper bound, not exact.
DELETE /policies/
Delete a policy. Takes effect immediately.
Auth: admin
Response: 204, or 404.
POST /policies/activate-baseline
Activate a set of pre-built safety policies from the library. Idempotent — calling again when policies are already active is safe.
Auth: admin
Request:
{ "mode": "standard" }mode is "standard" or "strict".
standard— activates 4 Dangerous Operations policiesstrict— activates those 4 plus 4 Data Protection policies
Response 200:
{
"mode": "standard",
"activated": ["block_shell_execution", "block_file_deletion", "block_cloud_metadata_access", "block_sensitive_file_reads"]
}Returns 422 for invalid mode.
POST /policies/nl-draft
Enterprise feature. Describe a policy in plain English and get back a scoped, compile-checked draft. Never writes to the policies table.
Auth: admin + enterprise license
Request:
{ "description": "Flag any transfer over $10,000 for review", "agent_id": null }Response 200:
{
"draft": {
"principal_type": null,
"principal_id": null,
"action_tool": null,
"resource_system": null,
"effect": "review",
"condition": { "numeric_conditions": { "amount": { "gt": 10000 } } }
},
"sentence": "Flag any call where amount > 10000 for review.",
"status": "ok",
"warnings": []
}If the draft doesn't compile, draft is null, status is "requires_manual_authoring", and warnings explains why — a draft is never handed back looking ready when it isn't.
POST /policies/simulate
Enterprise feature. Replay a draft (from nl-draft, or hand-written) against real historical audit_events. Read-only — writes neither a policy nor an audit event.
Auth: admin + enterprise license
Request:
{
"draft": {
"principal_type": null, "principal_id": null,
"action_tool": null, "resource_system": null,
"effect": "review",
"condition": { "numeric_conditions": { "amount": { "gt": 10000 } } }
},
"window_days": 7
}Response 200:
{
"eligible_events": 340,
"would_deny": 0,
"would_review": 12,
"corpus_start": "2026-05-01T00:00:00Z",
"corpus_note": null,
"matches": [
{ "audit_event_id": "uuid", "tool_name": "initiate_transfer", "decision": "review", "occurred_at": "2026-05-14T09:12:00Z" }
]
}If no events fall in the simulation's eligible corpus, would_deny/would_review are null and corpus_note explains why — a false "0 of 0" is never returned as if it meant "this policy changes nothing." The draft is compile-checked before replay; a draft that doesn't compile returns 422.
Rate-limit and token-budget aggregates are zeroed during simulation — those reflect point-in-time state and can't be honestly replayed against a historical event.
Reviews (HITL)
GET /reviews/
Get the status of a human review.
Auth: agent or admin
Response 200:
{
"id": "f1e2d3c4-...",
"audit_event_id": "3f2a1c8b-...",
"session_id": "b3e1f9a2-...",
"tool_name": "export_customer_data",
"tool_parameters": "{\"limit\": 500}",
"response_deadline": "2026-06-03T14:00:00Z",
"assigned_to": null,
"status": "pending",
"reviewer": null,
"review_note": null,
"reviewed_at": null,
"created_at": "2026-06-03T10:00:00Z"
}status is pending, approved, or denied.
GET /reviews
List reviews.
Auth: human JWT (any role) + business license
Query parameters: status (pending/approved/denied), limit (default 50, max 200), offset
Response 200: array of review objects
Returns 402 for community installations.
PATCH /reviews/
Approve or deny a pending review from the dashboard.
Auth: human JWT (any role) + business license
Request: { "action": "approve" } or { "action": "deny", "note": "reason text" }
Response 200: updated review object. Returns 409 if the review was already resolved
(approved or denied) by a prior call — resolution is row-locked, shared with the Slack
approve/deny path, so a concurrent Slack and dashboard resolution on the same review can't
both succeed. Returns 402 for community installations.
If your agent retries the identical call (same session_id, tool name, and parameters)
within HITL_RETRY_WINDOW_MINUTES (15 minutes by default) of an approve here, that retry
is automatically allowed instead of being routed to review again.
Audit Events
GET /audit-events
List audit events with optional filters. Community plan: limited to events from the last 7 days.
Auth: admin (human JWT)
Query parameters:
| Parameter | Description |
|---|---|
decision | Filter by allow, deny, or review |
agent_id | Filter by agent UUID |
tool_name | Exact tool name |
date_from | ISO 8601 start |
date_to | ISO 8601 end |
limit | Default 50 |
offset | Pagination offset |
Response 200: array including agent_name, policy_name, policy_id, workflow, sequence_number, tool_response (truncated 200 chars; always null via the MCP gateway path — not currently recorded). Each event where a policy fired includes a nested policy object (name, scope fields, effect) so the caller doesn't need a second lookup to render what happened.
GET /audit-events/export
Export filtered audit events as CSV. Accepts the same filters as GET /audit-events. Maximum 10,000 rows.
Auth: admin (human JWT) + enterprise license
Response: text/csv with Content-Disposition: attachment; filename=audit_events.csv
Returns 402 for community installations.
POST /audit-export-configs
Register an outbound SIEM export target. Every audit event written after creation is streamed to it — see Audit Log: SIEM Export.
Auth: admin (human JWT) + business license
Body:
| Field | Description |
|---|---|
export_type | otel or webhook |
target_url | Destination URL — an OTLP/HTTP collector endpoint for otel, a receiver URL for webhook |
enabled | Default true |
tenant_id | Optional |
Response 201: the created config (id, export_type, target_url, enabled, tenant_id).
Returns 402 for community installations.
GET /audit-export-configs
List all configured SIEM export targets.
Auth: admin (human JWT) + business license
Response 200: array of configs, same shape as the create response.
DELETE /audit-export-configs/
Remove a SIEM export target. Stops future exports; does not affect events already delivered.
Auth: admin (human JWT) + business license
Response 204: empty. Returns 404 if the config doesn't exist.
Sessions
GET /sessions
List agent sessions with enriched metadata.
Auth: human JWT (any role) + enterprise license
Response 200: array including agent_name, event_count, has_pending_review, completed_at.
Returns 402 for community/business installations.
GET /sessions//events
Get all gateway call events for a session, ordered by sequence_number.
Auth: human JWT (any role) + enterprise license
Response 200: array of audit events including trigger_context.
Returns 402 for community/business installations.
Dashboard
GET /dashboard/summary
Returns current operational summary. Polled by the dashboard Overview page.
Auth: admin (human JWT)
Response 200:
{
"intercepts_today": 47,
"allowed_today": 40,
"denied_today": 5,
"reviewed_today": 2,
"deny_rate_today": 0.12,
"pending_reviews": 3,
"active_warnings": 1,
"overdue_reviews": 0,
"top_denied_tool": { "tool": "query_credit_bureau", "count": 3 },
"high_risk_sessions": 0,
"active_sessions": 12,
"active_agents": 7,
"active_policies": 18,
"top_tools": [{"tool": "read_loan_application", "count": 22}],
"decisions_by_hour": [{"hour": "2026-06-03T10:00:00Z", "allow": 10, "deny": 2, "review": 1}]
}active_warnings is always 0 for community edition. top_denied_tool is null when no deny events today.
GET /dashboard/metrics
Returns analytics for the Decision metrics page.
Auth: admin (human JWT)
Response 200:
{
"policy_hit_rate": 0.34,
"deny_trend": [{"date": "2026-06-01", "allow": 120, "deny": 15, "review": 5}],
"top_agents_by_deny_rate": [{"agent_name": "loan-agent", "deny_count": 10, "deny_rate": 0.22}],
"avg_review_seconds": 342
}GET /dashboard/activity-log
List user activity log entries (admin actions on policies, agents, users).
Auth: admin (human JWT)
Query parameters: action, date_from, date_to, limit, offset
Response 200: array of { id, user_email, action, resource_type, resource_id, before_state, after_state, ip_address, created_at }
Users
GET /users
List all dashboard users.
Auth: admin (human JWT)
Response 200:
[
{
"id": "uuid",
"email": "alice@acme.com",
"name": "Alice Smith",
"role": "admin",
"is_active": true,
"is_root": true,
"password_set": true,
"last_login": "2026-06-03T09:00:00Z",
"created_at": "2026-06-02T14:00:00Z"
}
]POST /users
Invite a new dashboard user. Returns a one-time magic link for the user to set their password.
Auth: admin (human JWT)
Request: { "email": "bob@acme.com", "name": "Bob Jones", "role": "viewer" }
role is admin, analyst, or viewer.
Response 201:
{
"id": "uuid",
"email": "bob@acme.com",
"magic_link": "http://localhost:3000/invite?token=<plaintext>"
}The magic link is shown once — share it with the user securely.
PUT /users/
Update a user's name, role, or active status.
Auth: admin (human JWT)
Request: { "is_active": false } or { "role": "admin" }
Response 200: updated user object.
DELETE /users/
Delete a user. Root user cannot be deleted.
Auth: admin (human JWT)
Response: 204, or 404.
POST /users//regenerate-invite
Generate a new magic link for a user. Invalidates any previous link. Works for both pending users (never set password) and active users (password reset).
Auth: admin (human JWT)
Response 200: { "magic_link": "http://localhost:3000/invite?token=<plaintext>" }
Org Settings
GET /org-settings
Get the organisation singleton settings.
Auth: human JWT (any role)
Response 200:
{
"org_name": "Acme Corp",
"timezone": "America/New_York"
}PUT /org-settings
Update organisation settings. All fields optional (patch semantics).
Auth: admin (human JWT)
Request: { "org_name": "Acme Corp", "timezone": "Europe/London" }
timezone must be a valid IANA timezone string.
Response 200: updated org settings object.
License
GET /license-info
Returns the current license plan. Used by the frontend at runtime to unlock enterprise features. Safe to call unauthenticated — never returns the raw license key or email.
Auth: none required
Response 200:
{
"plan": "enterprise",
"company": "Acme Corp",
"is_enterprise": true,
"is_business": true,
"expires_at": "2027-06-03T00:00:00Z"
}plan is community, business, enterprise, or trial. Returns community when no org_settings row exists, or when one exists but license_plan isn't set.
GET /license/features
Returns which paid destinations exist for this install and their feature flags, for the dashboard to decide what to render in its navigation. A paid destination is absent entirely on a lower tier, never shown locked or greyed out.
Auth: human JWT (any role)
Response 200:
{
"tier": "enterprise",
"features": {
"nl_authoring": true,
"simulation": true,
"hitl": true,
"compliance_reports": true
},
"license_status": "active"
}tier is "free", "business", or "enterprise" — reflects the plan the org is licensed for, regardless of license_status, so a past_due/canceled Business or Enterprise org still sees its nav items (routed to a reactivate banner) instead of losing them outright. license_status is "active", "past_due", "canceled", or null (community, or a paid plan that hasn't synced yet).
PUT /settings/license-activation
The only way an activation code enters the system — there's no install.sh
prompt for it. Validates the code against billing.aictl.io before saving it;
a code that doesn't validate is never stored.
Auth: admin (human JWT)
Request: { "activation_code": "..." }
Response 200:
{
"license_status": "active",
"license_plan": "business",
"license_synced_at": "2026-09-21T10:00:00Z"
}Response 400: invalid or unrecognized activation code — nothing is saved.
Billing
GET /billing/usage
Returns plan details and gateway call usage for the current and previous calendar month.
Auth: admin (human JWT)
Response 200:
{
"plan": "business",
"company": "Acme Corp",
"annual_price_usd": 999.0,
"retention_days": 30,
"features": ["Cedar policy enforcement", "..."],
"this_month": { "period": "2026-09", "intercepts": 124500 },
"last_month": { "period": "2026-08", "intercepts": 98000 },
"manage_subscription_url": "https://billing.aictl.io/portal/...",
"upgrade_url": null,
"license_status": "active",
"license_synced_at": "2026-09-21T10:00:00Z"
}features is the cascaded list for the plan (community features, plus
business features if applicable, plus enterprise features if applicable) —
not a fixed enum of internal flag names. manage_subscription_url is null
until an activation code has been entered; upgrade_url is always null
today (see Operations → Billing and usage).
Enterprise Compliance
POST /enterprise/compliance/report
Generate a compliance report narrative using the configured LLM.
Auth: admin + enterprise license
Request:
{
"frameworks": ["soc2", "eu_ai_act"],
"date_from": "2026-01-01",
"date_to": "2026-05-31",
"format": "pdf"
}frameworks values: eu_ai_act, nist_ai_rmf, soc2, iso_42001.
format: pdf or md.
Set LLM_MOCK_ENABLED=true to generate a placeholder without an LLM API key.
Response: Binary file download (application/pdf or text/markdown).
GET /enterprise/compliance/reports
List previously generated compliance reports.
Auth: admin + enterprise license
Response 200: array of { id, frameworks, date_from, date_to, format, generated_at, generated_by_email, llm_model, token_input, token_output }
GET /enterprise/compliance/reports//download
Download a previously generated report.
Auth: admin + enterprise license
Response: Binary file download.
Health
GET /health
API liveness check. No authentication required. Use for load balancer health checks.
Response 200 (community, no license):
{
"status": "ok",
"service": "aicontrol",
"policy_engine_status": "in_process",
"drift_detector_status": "enterprise_only"
}policy_engine_status is always "in_process" on every tier — Cedar evaluates in the same process as the API, so there's no separate service to be unreachable.
drift_detector_status reflects an internal background consistency check (agent-approved tools vs. policy coverage) and is only meaningful on an enterprise-licensed install, where it returns "healthy" or "degraded" instead of "enterprise_only".
Error responses
| Code | Meaning |
|---|---|
| 400 | Bad request |
| 401 | Missing or invalid token |
| 402 | Enterprise license required |
| 403 | Token lacks permission |
| 404 | Resource not found |
| 409 | Conflict (e.g. setup already complete) |
| 422 | Validation error — field-level detail in body |
| 500 | Internal server error |