REST API Reference (Gateway)
Interactive API Reference
Open Swagger UI — Browse all endpoints, try requests, and view schemas interactively.
Spec Downloads
1. Auth and Session
GET /api/v1/auth/config
- Auth: public
- Request body: none
- Response: auth capability object. When SSO providers are configured, the gateway advertises the published SAML and OIDC connection details used by the dashboard and IdP operators.
{
"password_enabled": true,
"user_auth_enabled": true,
"saml_enabled": false,
"saml_enterprise": false,
"saml_login_url": "",
"saml_metadata_url": "",
"session_ttl": "24h",
"require_rbac": true,
"require_principal": false,
"default_tenant": "default",
"oidc_enabled": false,
"oidc_issuer": "",
"oidc_login_url": "",
"oidc_client_id": "",
"oidc_redirect_uri": "",
"oidc_scopes": [
"openid",
"profile",
"email"
],
"oidc_client_secret_masked": ""
}
GET /api/v1/auth/sso/oidc/login
- Auth: public
- License gate: requires the
SSOentitlement - Query params:
redirect(optional) — same-origin absolute or relative post-auth URL; when omitted, the gateway falls back toCORDUM_AUTH_REDIRECT_URLor/login
- Response:
302 Foundredirect to the OIDC authorization endpoint - Errors:
403 tier_limit_exceeded,404,500
GET /api/v1/auth/sso/oidc/callback
- Auth: public
- License gate: requires the
SSOentitlement - Query params:
state— state token created by the login endpointcode— authorization code from the IdPerror/error_description— optional IdP error details
- Success behavior:
- exchanges the authorization code for tokens
- validates the returned ID token against the discovered issuer JWKS
- provisions or updates the mapped Cordum user
- creates a Redis-backed browser session
- sets the
cordum_sessionHttpOnly cookie - redirects to the resolved UI target with a hash fragment containing
token,expires_at,user_id,username,email,display_name,role, andtenant - when no redirect target is available, returns JSON with
token,expires_at, anduser
- Errors:
400,401,403 tier_limit_exceeded,404,500
GET /api/v1/auth/sso/saml/metadata
- Auth: public
- License gate: requires both SSO and SAML entitlements
- Response: service-provider metadata XML (
application/samlmetadata+xml) - Errors:
403 tier_limit_exceeded,404,500
GET /api/v1/auth/sso/saml/login
- Auth: public
- License gate: requires both SSO and SAML entitlements
- Query params:
redirect(optional) — same-origin absolute or relative post-auth URL; when omitted, the gateway falls back toCORDUM_AUTH_REDIRECT_URLor/loginon the configured UI origin
- Response:
302 Foundredirect to the IdP for redirect binding, or an HTML auto-submit form for POST binding - Errors:
400,403 tier_limit_exceeded,404,500
POST /api/v1/auth/sso/saml/acs
- Auth: public
- License gate: requires both SSO and SAML entitlements
- Content type:
application/x-www-form-urlencoded - Form fields:
SAMLResponse— signed assertion from the IdPRelayState— optional state key created by the login endpoint
- Success behavior:
- creates a Redis-backed browser session
- sets the
cordum_sessionHttpOnly cookie - redirects to the resolved UI target with a hash fragment containing
token,expires_at,user_id,username,email,display_name,role, andtenant
- when no redirect target is available, returns JSON with
token,expires_at, anduser - Errors:
400,401,403 tier_limit_exceeded,404,500
SCIM 2.0 provisioning routes
- Auth: public to the API-key middleware, but each route requires
Authorization: Bearer <scim-token> - License gate: requires the
SCIMentitlement - Media type:
application/scim+json - Tenant behavior: routes operate on the gateway default tenant
- Error shape: RFC 7644-style SCIM error payloads with
schemas,detail,status, and optionalscimType
Provisioning endpoints:
GET /api/v1/scim/v2/ServiceProviderConfigGET /api/v1/scim/v2/SchemasGET /api/v1/scim/v2/ResourceTypesGET|POST /api/v1/scim/v2/UsersGET|PUT|PATCH|DELETE /api/v1/scim/v2/Users/{id}GET|POST /api/v1/scim/v2/GroupsGET|PUT|PATCH|DELETE /api/v1/scim/v2/Groups/{id}
Behavior notes:
POST /Userscreates or provisions a Redis-backed Cordum userDELETE /Users/{id}is a soft delete that disables the user instead of removing the Redis record- group membership updates map SCIM groups onto Cordum roles and update referenced users
- list endpoints support
filter,startIndex,count,sortBy, andsortOrder - discovery endpoints advertise PATCH/filter/sort support and the bearer-token auth scheme
Common SCIM request fields:
- user payloads:
userName,displayName,name.givenName,name.familyName,emails[].value,active,roles[].value - group payloads:
displayName,externalId,members[].value
Common errors:
401invalid or missing SCIM bearer token403 tier_limit_exceededSCIM entitlement disabled404SCIM resource not found409 uniquenessduplicate username/email/group name
GET /api/v1/scim/settings
- Auth: required
- Role:
admin - License gate: requires the
SCIMentitlement - Response: dashboard settings payload containing
endpointUrl, token metadata, and the current SCIM-managed user list - Errors:
401,403,500
POST /api/v1/scim/settings/token
- Auth: required
- Role:
admin - License gate: requires the
SCIMentitlement - Response: generates or rotates a Redis-managed SCIM bearer token and returns the new token plus masked metadata
- Errors:
401,403,500
POST /api/v1/auth/login
- Auth: public
- Request schema (
AuthLoginRequest):
{
"username": "admin",
"password": "<password-or-api-key>",
"tenant": "default"
}
- Response schema (
AuthLoginResponse):
{
"token": "session-... or masked-api-key",
"expires_at": "2026-02-13T10:00:00Z",
"user": {
"id": "user-id",
"username": "admin",
"display_name": "Admin",
"tenant": "default",
"roles": ["admin"],
"source": "user|api_key",
"created_at": "...",
"updated_at": "...",
"last_login_at": "..."
}
}
- Errors:
400,401,403,429,500
GET /api/v1/auth/session
- Auth: required
- Request body: none
- Response: same shape as login response
- Errors:
401
POST /api/v1/auth/logout
- Auth: required
- Request body: none
- Response:
204 No Content
POST /api/v1/auth/password
- Auth: required (user auth mode)
- Request schema (
ChangePasswordRequest):
{
"current_password": "old",
"new_password": "new"
}
- Response:
204 No Content - Errors:
400,401,404
Example: login + session check
curl -sS -X POST http://localhost:8081/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"YOUR_API_KEY","tenant":"default"}'
curl -sS http://localhost:8081/api/v1/auth/session \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default'
2. User Management
All endpoints below require admin role and tenant-scoped access.
POST /api/v1/users
- Auth: required + admin
- Request schema (
CreateUserRequest):
{
"username": "alice",
"password": "strong-password",
"tenant": "default",
"role": "user"
}
- Response:
201+AuthUser - Errors:
400,401,403,409
GET /api/v1/users
- Auth: required + admin
- Request body: none
- Response:
{
"items": [
{
"id": "...",
"username": "alice",
"display_name": "",
"tenant": "default",
"roles": ["user"],
"created_at": "...",
"updated_at": "..."
}
]
}
- Errors:
400,401,403,500
PUT /api/v1/users/{id}
- Auth: required + admin
- Request schema:
{
"display_name": "Alice",
"roles": ["admin"]
}
- Response: updated
AuthUser - Errors:
400,401,403,404,500
DELETE /api/v1/users/{id}
- Auth: required + admin
- Request body: none
- Response:
204 - Errors:
400,401,403,404,500
POST /api/v1/users/{id}/password
- Auth: required + admin
- Request schema:
{
"password": "new-strong-password"
}
- Response:
204 - Errors:
400,401,403,404,500
Example: create user
curl -sS -X POST http://localhost:8081/api/v1/users \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-H 'Content-Type: application/json' \
-d '{"username":"alice","password":"Secret123!","role":"user"}'
3. API Key Management
All endpoints require admin role.
GET /api/v1/auth/keys
- Auth: required + admin
- Response:
{
"items": [
{
"id": "key-id",
"name": "ci-key",
"prefix": "abc123",
"scopes": ["jobs:write"],
"createdAt": "...",
"lastUsed": "...",
"usageCount": 12,
"expiresAt": "..."
}
]
}
- Errors:
403,500,503
POST /api/v1/auth/keys
- Auth: required + admin
- Request schema:
{
"name": "ci-key",
"scopes": ["jobs:write", "workflows:read"],
"expiresAt": "2026-12-31T23:59:59Z"
}
- Response:
201
{
"key": {
"id": "...",
"name": "ci-key",
"prefix": "...",
"scopes": ["jobs:write"],
"createdAt": "...",
"usageCount": 0
},
"secret": "raw-api-key-once"
}
- Errors:
400,403,500,503
DELETE /api/v1/auth/keys/{id}
- Auth: required + admin
- Response:
204 - Errors:
400,403,404,500,503
Example: create API key
curl -sS -X POST http://localhost:8081/api/v1/auth/keys \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-H 'Content-Type: application/json' \
-d '{"name":"ci-key","scopes":["jobs:write"]}'
3.5 RBAC Role Management
All endpoints require admin role. Write operations (PUT, DELETE) require the RBAC license entitlement — without it, they return 403 tier_limit_exceeded.
GET /api/v1/auth/roles
- Auth: required + admin
- Response:
{
"roles": [
{
"name": "admin",
"description": "Full access to all resources",
"permissions": ["admin.*"],
"inherits": [],
"built_in": true,
"created_at": "2026-04-11T19:00:00Z",
"updated_at": "2026-04-11T19:00:00Z"
}
],
"entitled": true
}
- Errors:
403,500,503
GET /api/v1/auth/roles/{name}
- Auth: required + admin
- Response:
{
"role": { "name": "operator", "..." : "..." },
"resolved_permissions": ["jobs.read", "jobs.write", "..."]
}
resolved_permissionsincludes inherited permissions flattened from the hierarchy- Errors:
400,403,404,500
PUT /api/v1/auth/roles/{name}
- Auth: required + admin + RBAC entitlement
- Request:
{
"description": "DevOps engineer role",
"permissions": ["jobs.read", "jobs.write", "config.read", "config.write"],
"inherits": ["viewer"]
}
- Creates a new role or updates an existing one
- Validates inheritance: rejects circular inheritance and unknown parent roles
- Built-in roles: description and permissions can be updated, but inheritance cannot change
- Errors:
400(validation),403(not admin or not entitled),500,503
DELETE /api/v1/auth/roles/{name}
- Auth: required + admin + RBAC entitlement
- Cannot delete built-in roles (admin, operator, viewer)
- Response:
{"deleted": true, "name": "role_name"} - Errors:
400(built-in role),403,404,500,503
Available Permissions
| Permission | Description |
|---|---|
admin.* | Full access (wildcard) |
jobs.read | View jobs |
jobs.write | Create/edit jobs |
jobs.approve | Approve jobs |
workflows.read | View workflows |
workflows.write | Create/edit workflows |
workers.read | View workers |
config.read | View configuration |
config.write | Edit configuration |
audit.read | View audit log |
packs.install | Install packs |
packs.uninstall | Uninstall packs |
policy.read | View policies |
policy.write | Edit policies |
schemas.read | View schemas |
schemas.write | Edit schemas |
users.read | View users |
users.write | Manage users |
roles.read | View roles |
roles.write | Manage roles |
3.6 Audit Export (SIEM)
All endpoints require admin role. Entitlement-gated by SIEMExport or AuditExport.
GET /api/v1/audit/export/health
- Auth: required + admin + SIEM entitlement
- Response:
{
"backend": "webhook",
"status": "active",
"entitled": true
}
GET /api/v1/audit/export/config
- Auth: required + admin
- Returns non-sensitive backend configuration (URLs, regions, flags — never secrets)
- Response varies by backend type
POST /api/v1/audit/export/test
- Auth: required + admin + SIEM entitlement
- Sends a test
SIEMEventto the configured export backend - Response:
{"success": true, "message": "test event sent to webhook backend"} - Errors:
400(no backend configured),403(not entitled),500
3.7 Legal Hold
Per-tenant immutable retention holds on audit data. All endpoints require admin role and LegalHold entitlement.
POST /api/v1/audit/legal-hold
- Auth: required + admin + LegalHold entitlement
- Request:
{
"tenant_id": "default",
"reason": "Litigation pending — case #12345"
}
tenant_iddefaults to the server default tenant if omitted- Duplicate hold on same tenant returns
409 - Response:
201with{"hold": {...}} - Errors:
400(missing reason),403(not entitled),409(active hold exists),500
GET /api/v1/audit/legal-holds
- Auth: required + admin + LegalHold entitlement
- Query params:
?tenant=<id>(optional filter) - Response:
{"holds": [...]}
DELETE /api/v1/audit/legal-hold/{id}
- Auth: required + admin + LegalHold entitlement
- Releases the hold. Does NOT delete retained data — normal TTL resumes.
- Response:
{"released": true, "id": "..."} - Errors:
404(hold not found),409(already released),403,500
4. Jobs and Traces
POST /api/v1/jobs
- Auth: required
- Request schema (
submitJobRequest):
{
"prompt": "Generate plan",
"topic": "job.default",
"adapter_id": "openai:gpt-4.1",
"priority": "interactive",
"context": {},
"memory_id": "run:abc",
"context_mode": "balanced",
"tenant_id": "default",
"org_id": "default",
"team_id": "team-a",
"project_id": "proj-a",
"principal_id": "user-123",
"actor_id": "user-123",
"actor_type": "human",
"idempotency_key": "job-abc-001",
"pack_id": "pack.example",
"capability": "summarize",
"risk_tags": ["pii"],
"requires": ["approval"],
"labels": {"k":"v"},
"max_input_tokens": 8000,
"allow_summarization": true,
"allow_retrieval": true,
"tags": ["demo"],
"max_output_tokens": 1024,
"max_total_tokens": 4096,
"deadline_ms": 30000
}
- Response:
{
"job_id": "uuid",
"trace_id": "uuid"
}
- Errors:
400,403,409,429,503
GET /api/v1/jobs
- Auth: required
- Query:
limit,cursor,state,topic,tenant,team,trace_id,updated_after,updated_before
- Response:
{
"items": [
{
"id": "job-id",
"state": "PENDING",
"topic": "job.default",
"tenant": "default",
"updated_at": 1739400000000000
}
],
"next_cursor": 1739399999999999
}
- Errors:
403,500,503
GET /api/v1/jobs/{id}
- Auth: required + tenant access
- Response (abridged):
{
"id": "job-id",
"state": "RUNNING",
"trace_id": "trace-id",
"context_ptr": "redis://ctx:job-id",
"result_ptr": "redis://res:job-id",
"result": {},
"topic": "job.default",
"tenant": "default",
"capability": "summarize",
"risk_tags": ["pii"],
"requires": ["approval"],
"output_safety": {},
"labels": {},
"workflow_id": "...",
"run_id": "...",
"step_id": "..."
}
- Errors:
400,403,404
GET /api/v1/jobs/{id}/decisions
- Auth: required + tenant access
- Query:
limit - Response: array of safety decision records for the job
- Errors:
400,403,500,503
POST /api/v1/jobs/{id}/cancel
- Auth: required + admin + tenant access
- Request body: none
- Response:
{
"id": "job-id",
"state": "CANCELLED"
}
- Errors:
400,403,404,500
POST /api/v1/jobs/{id}/remediate
- Auth: required + admin + tenant access
- Request schema:
{
"remediation_id": "optional-if-single-choice"
}
- Response:
{
"job_id": "new-job-id",
"trace_id": "trace-id"
}
- Errors:
400,403,404,409,502,503
GET /api/v1/traces/{id}
- Auth: required
- Response: list of trace job records visible to caller tenant
- Errors:
400,500
Example: submit + fetch job
curl -sS -X POST http://localhost:8081/api/v1/jobs \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: demo-job-001' \
-d '{"prompt":"hello","topic":"job.default"}'
curl -sS http://localhost:8081/api/v1/jobs/JOB_ID \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default'
5. DLQ (Dead Letter Queue)
All DLQ endpoints require admin role.
GET /api/v1/dlq
- Auth: required + admin
- Query:
limit - Response: array of DLQ entries
- Errors:
403,500,503
GET /api/v1/dlq/page
- Auth: required + admin
- Query:
limit,cursor - Response:
{
"items": [
{
"job_id": "job-id",
"topic": "job.default",
"status": "JOB_STATUS_FAILED",
"reason": "error text",
"created_at": "..."
}
],
"next_cursor": 1739400000
}
- Errors:
403,500,503
DELETE /api/v1/dlq/{job_id}
- Auth: required + admin + tenant access
- Response:
204 - Errors:
400,403,500,503
POST /api/v1/dlq/{job_id}/retry
- Auth: required + admin + tenant access
- Request body: none
- Response:
{
"job_id": "new-retry-job-id"
}
- Errors:
400,403,404,500,503
Example: retry DLQ entry
curl -sS -X POST http://localhost:8081/api/v1/dlq/JOB_ID/retry \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default'
Remaining Endpoint Groups
The remaining groups (workflows, policy bundles, config, packs/marketplace, locks, schemas, artifacts, websocket stream, MCP) are documented below in this same file.
6. Workflows and Runs
Workflow definitions
GET /api/v1/workflows
- Auth: required
- Query:
org_id - Response: array of workflow definitions
- Errors:
403,500,503
POST /api/v1/workflows
- Auth: required + admin
- Request schema (
createWorkflowRequest):
{
"id": "optional-uuid",
"org_id": "default",
"team_id": "team-a",
"name": "Deploy workflow",
"description": "...",
"version": "1.0.0",
"timeout_sec": 600,
"created_by": "user-1",
"input_schema": {},
"parameters": [],
"steps": {},
"config": {}
}
- Response:
201+{ "id": "workflow-id" } - Errors:
400,403,500,503
GET /api/v1/workflows/{id}
- Auth: required + tenant access
- Response: workflow definition JSON
- Errors:
400,403,404,503
DELETE /api/v1/workflows/{id}
- Auth: required + admin + tenant access
- Response:
204 - Errors:
400,403,404,500,503
Run management
POST /api/v1/workflows/{id}/runs
- Auth: required + admin
- Query:
org_id,team_id,dry_run - Body: arbitrary workflow input object
- Idempotency key supported
- Response:
{ "run_id": "run-uuid" } - Errors:
400,403,404,409,429,500,503
GET /api/v1/workflows/{id}/runs
- Auth: required + tenant access
- Response: array of run objects
- Errors:
400,403,500,503
GET /api/v1/workflow-runs
- Auth: required
- Query:
limit,cursor,status,workflow_id,org_id,team_id,updated_after,updated_before - Response:
{
"items": [ { "id": "run-id", "workflow_id": "wf-id", "status": "RUNNING" } ],
"next_cursor": 1739400000
}
GET /api/v1/workflow-runs/{id}
- Auth: required + tenant access
- Response: run object enriched with active delay timers (if any)
{
"id": "run-id",
"workflow_id": "wf-id",
"status": "RUNNING",
"timers": [
{
"workflow_id": "wf-id",
"run_id": "run-id",
"fires_at": "2026-02-20T12:30:00Z",
"remaining_ms": 28500
}
]
}
The timers array is omitted when no active delay timers exist for the run. Timer lookup is best-effort — failures are silently ignored to avoid degrading the endpoint.
- Errors:
400,403,404,503
GET /api/v1/workflow-runs/{id}/timeline
- Auth: required + tenant access
- Query:
limit - Response: array of timeline events
- Errors:
400,403,500,503
DELETE /api/v1/workflow-runs/{id}
- Auth: required + admin + tenant access
- Response:
204 - Errors:
400,403,404,500,503
POST /api/v1/workflow-runs/{id}/rerun
- Auth: required + admin + tenant access
- Request schema:
{
"from_step": "optional-step-id",
"dry_run": false
}
- Response:
{ "run_id": "new-run-id" } - Errors:
400,403,404,429,503
POST /api/v1/workflows/{id}/dry-run
- Auth: required + admin + tenant access
- Request schema:
{
"input": {},
"environment": "staging"
}
- Response schema (
dryRunResponse):
{
"workflow_id": "wf-id",
"steps": [
{
"step_id": "step-a",
"step_name": "Step A",
"step_type": "worker",
"decision": "ALLOW",
"reason": "...",
"rule_id": "rule-1"
}
]
}
- Errors:
400,403,404,503
Chat on runs
GET /api/v1/workflow-runs/{id}/chat
- Auth: required + tenant access
- Query:
limit,cursor - Response (
chatResponse):
{
"items": [
{
"id": "msg-id",
"run_id": "run-id",
"role": "user|agent|system",
"content": "text",
"step_id": "...",
"job_id": "...",
"agent_id": "...",
"agent_name": "...",
"created_at": "2026-02-13T09:00:00Z",
"metadata": {}
}
],
"next_cursor": 10
}
POST /api/v1/workflow-runs/{id}/chat
- Auth: admin or operator role required + tenant access
- Request:
{
"content": "Please continue",
"role": "user",
"step_id": "optional",
"job_id": "optional",
"agent_id": "optional",
"agent_name": "optional",
"metadata": {}
}
- Role field rules:
- Allowed values:
user,agent,assistant(alias for agent),system. Unrecognized values return400. - Omitting
roledefaults touser. - Only admin callers may set
roletoagentorsystem. Operator callers are forced touser.
- Allowed values:
- Response: created chat message (same shape as
items[]) - Errors:
400,403,404,500,503
POST /api/v1/workflows/{id}/runs/{run_id}/cancel
- Auth: required + admin + tenant access
- Request body: none
- Response:
204 - Errors:
400,403,409,503
Example: start workflow run
curl -sS -X POST http://localhost:8081/api/v1/workflows/WF_ID/runs \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: run-001' \
-d '{"input":"hello"}'
7. Approvals (Job Approval Queue)
GET /api/v1/approvals
- Auth: required + admin
- Query:
limit,cursor,include_resolved(falseto show pending approvals only) - Response:
{
"items": [
{
"job": { "id": "job-id", "state": "APPROVAL" },
"decision": "REQUIRE_APPROVAL",
"policy_snapshot": "cfg:...",
"policy_rule_id": "rule-1",
"policy_reason": "finance approval required",
"constraints": {},
"job_hash": "...",
"approval_required": true,
"approval_ref": "job-id",
"approval_status": "approved",
"approval_actionability": "resolved",
"approval_revision": 2,
"approval_decision": "approve",
"workflow_id": "wf-1",
"workflow_run_id": "run-1",
"workflow_step_id": "approve",
"step_name": "Manager Approval",
"gate_type": "workflow_approval",
"context_ptr": "ctx:mem:job-workflow-context",
"job_input": {
"kind": "workflow_approval_context",
"version": 1,
"workflow": {
"workflow_id": "wf-1",
"run_id": "run-1",
"step_id": "approve",
"step_name": "Manager Approval"
},
"decision": {
"amount": 1250,
"currency": "USD",
"vendor": "Acme Travel",
"approval_reason": "manager threshold exceeded",
"next_effect": "Approve to continue Manager Approval."
}
},
"decision_summary": {
"source": "workflow_payload",
"completeness": "rich",
"context_status": "available",
"title": "Review Acme Travel spending request",
"why": "manager threshold exceeded",
"next_effect": "Approve to continue Manager Approval.",
"amount": 1250,
"currency": "USD",
"vendor": "Acme Travel"
},
"resolved_by": "manager-2",
"resolved_comment": "ticket INC-123",
"resolution": "approved",
"resolved_at": 1709000002000000
}
],
"next_cursor": 1739400000000000
}
Approval lifecycle endpoints may return structured conflict payloads on 409:
{
"error": "approval in progress; retry",
"status": 409,
"code": "approval_retryable_lock",
"retryable": true
}
Notes:
- Workflow approval gates and policy approvals share the same list endpoint.
decision_summaryis the primary decision-first contract for the dashboard. It highlights what is being approved, why it matters, and what happens next.context_ptrandjob_inputare only present when a workflow payload was persisted and successfully hydrated. Older non-workflow approvals may omit them.decision_summary.context_statuscan beavailable,missing,malformed,unavailable, orabsentso degraded workflow data fails informatively instead of appearing as an empty approval shell.decision_summary.sourcedistinguishes rich workflow payloads (workflow_payload), workflow-label fallback (workflow_labels), and legacy/policy-only approvals (policy_only).approval_statusis the explicit lifecycle state. Values:pending,approved,rejected,expired,invalidated,repaired.approval_actionabilitytells the dashboard whether the approval can still be acted on. Values:actionable,resolved,expired,invalidated,repaired.approval_revisionincrements on each lifecycle mutation and helps clients detect stale views or concurrent operator repair work.approval_decisionrecords the lifecycle mutation that resolved or repaired the approval (approve,reject,expire,invalidate,repair).- Resolved approvals continue to expose
decision_summary,policy_snapshot,job_hash,resolved_by,resolved_comment, andresolutionfor audit/history views.
POST /api/v1/approvals/{job_id}/approve
- Auth: required + admin + tenant access
- Request:
{
"reason": "approved by on-call",
"note": "ticket INC-123"
}
- Response:
{
"job_id": "job-id",
"trace_id": "trace-id"
}
- Errors:
400,403,404,409,502,503
409 Conflict uses machine-readable code values:
approval_retryable_lock— another decision or repair is in progress; safe to retryapproval_terminal_run— the workflow run already moved past this approvalapproval_stale_snapshot— the governing policy snapshot changedapproval_stale_request— the underlying job request changedapproval_not_actionable— the approval is no longer actionableapproval_already_resolved— a decision is already recorded; refresh for audit detail
POST /api/v1/approvals/{job_id}/reject
- Auth: required + admin + tenant access
- Request:
{
"reason": "policy violation",
"note": "missing human approval"
}
- Response:
{ "job_id": "job-id" }
- Errors:
400,403,404,409,503
Reject conflicts use the same structured 409 payload and code values as the
approve endpoint.
POST /api/v1/approvals/{job_id}/repair
- Auth: required + admin + tenant access
- Request:
{
"apply": false,
"note": "optional operator note"
}
- Response (dry-run):
{
"job_id": "job-id",
"apply": false,
"applied": false,
"repairable": true,
"state": "APPROVAL_REQUIRED",
"trace_id": "trace-id",
"approval": {
"status": "approved",
"actionability": "resolved"
},
"plan": {
"kind": "invalidate_stale_snapshot",
"repairable": true,
"reason": "policy snapshot changed after approval request creation"
}
}
- Response (apply): same envelope, but
applied=true; the returnedapprovalobject reflects the repaired lifecycle record and may includepublish_deferred=true/publish_errorwhen state repair succeeded but the follow-up publish must be replayed by recovery logic. - Errors:
400,403,404,409,500
Notes:
apply=falseis the dry-run inspection path used by operators andcordumctl.plan.kindclassifies terminal workflow runs, stale request drift, stale policy snapshots, legacy partial approvals, and publish-intent recovery.- Repair requests also use
approval_retryable_lockwhen another decision or repair currently holds the approval lock.
GET /api/v1/approvals/{job_id}/context
- Auth: required + admin + tenant access
- Path params:
job_id(required) - Response schema:
{
"approval": {
"source": "workflow_payload",
"completeness": "rich",
"context_status": "available",
"title": "Review Acme Travel spending request",
"subject": "procurement-order-7823",
"why": "manager threshold exceeded",
"next_effect": "Approve to continue Manager Approval.",
"amount": 1250,
"currency": "USD",
"vendor": "Acme Travel",
"item_count": 3,
"items_preview": ["Flights", "Hotel", "Car rental"],
"escalation_reason": "spend > $500"
},
"blast_radius": {
"namespaces": ["production"],
"resources": ["payments", "vendor-contracts"],
"downstream_steps": ["payment-processor", "vendor-email"]
},
"prior_approvals": [
{
"job_id": "job-prev-1",
"topic": "job.b2b.procurement-pay",
"decision": "approved",
"resolved_at": 1709000002000000
}
],
"rollback_hint": "Cancel the workflow run to revert pending state.",
"policy_snapshot_summary": {
"snapshot": "cfg:policy:bundle:default:v3",
"rule_id": "finance-approval-required",
"decision": "REQUIRE_APPROVAL"
},
"time_remaining_ms": 86400000,
"constraints": {
"sandbox": true,
"timeout": 30
}
}
- Notes:
approvalis the decision briefing — what is being approved, why, and what happens next.blast_radiusshows affected systems, namespaces, and downstream workflow steps.prior_approvalslists up to 10 recent related approvals for the same tenant/topic.rollback_hintis operator guidance for reverting the action.time_remaining_msis milliseconds until the approval deadline expires (null if no deadline).constraintsare the safety constraints from the policy decision.- Fields may be absent when context data is unavailable (
context_statusindicates the reason).
- Errors:
400,403,404
8. Policy Evaluation and Bundles
Policy evaluation endpoints
POST /api/v1/policy/evaluate
POST /api/v1/policy/simulate
POST /api/v1/policy/explain
- Auth: required
- Request schema (
policyCheckRequest):
{
"job_id": "job-id",
"topic": "job.default",
"tenant": "default",
"org_id": "default",
"team_id": "team-a",
"workflow_id": "wf-id",
"step_id": "step-id",
"principal_id": "user-1",
"priority": "interactive",
"estimated_cost": 0.1,
"budget": {
"max_input_tokens": 8000,
"max_output_tokens": 1024,
"max_total_tokens": 4096,
"deadline_ms": 30000
},
"labels": {"k":"v"},
"memory_id": "run:abc",
"effective_config": {},
"meta": {
"tenant_id": "default",
"actor_id": "user-1",
"actor_type": "human",
"idempotency_key": "abc",
"capability": "summarize",
"risk_tags": ["pii"],
"requires": ["approval"],
"pack_id": "pack.example",
"labels": {}
}
}
- Response: protobuf-JSON
PolicyCheckResponsefrom safety kernel - Errors:
400,403,502,503
GET /api/v1/policy/snapshots
- Auth: required
- Response: protobuf-JSON
ListSnapshotsResponse - Errors:
502,503
Policy bundle management (admin)
GET /api/v1/policy/rules
- Query:
include_disabled=true|false - Response:
{
"items": [ { "id": "rule-id", "source": {"fragment_id":"secops/demo"} } ],
"errors": [ { "fragment_id": "...", "error": "..." } ]
}
GET /api/v1/policy/velocity-rules
- Auth: required + admin
- Response:
{
"items": [
{
"id": "login-burst",
"name": "Login burst guard",
"match": {
"topics": ["job.auth.login"],
"tenants": ["default"],
"risk_tags": ["auth"]
},
"window": "1m0s",
"key": "tenant",
"threshold": 3,
"decision": "require_approval",
"reason": "Repeated login attempts require review",
"enabled": true,
"created_at": "2026-04-11T20:00:00Z",
"updated_at": "2026-04-11T20:00:00Z"
}
],
"count": 1,
"limit": 20,
"updated_at": "2026-04-11T20:00:00Z",
"upgrade_url": ""
}
- Notes:
- Velocity rules are stored as policy bundle fragments under
velocity/{id}in the system policy config document. - Creation is gated by the
velocity_rulesentitlement. Community licenses return403 tier_limit_exceeded; Team is limited; Enterprise is unlimited.
- Velocity rules are stored as policy bundle fragments under
POST /api/v1/policy/velocity-rules
- Auth: required + admin
- Request:
{
"id": "login-burst",
"name": "Login burst guard",
"match": {
"topics": ["job.auth.login"],
"tenants": ["default"],
"risk_tags": ["auth"]
},
"window": "1m",
"key": "tenant",
"threshold": 3,
"decision": "require_approval",
"reason": "Repeated login attempts require review",
"enabled": true
}
- Response: created rule object (same shape as
GET /api/v1/policy/velocity-rules) - Errors:
400invalid rule schema (window,threshold, orkey)403tier_limit_exceeded409duplicate rule id
PUT /api/v1/policy/velocity-rules/{id}
- Auth: required + admin
- Request: same schema as create
- Response: updated rule object
- Errors:
400,403,404
DELETE /api/v1/policy/velocity-rules/{id}
- Auth: required + admin
- Response:
204 No Content - Errors:
400,404
GET /api/v1/policy/velocity-rules/stats
- Auth: required + admin
- Response:
{
"items": [
{
"id": "login-burst",
"hit_count_24h": 5,
"hit_rate_24h": 0.2083333333,
"current_window_count": 4,
"current_window_max": 4,
"active_buckets": 1,
"exceeded_buckets": 1,
"last_triggered": "2026-04-11T20:42:00Z",
"hourly_hits": [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2]
}
],
"top_rules": [
{
"id": "login-burst",
"hit_count_24h": 5
}
],
"generated_at": "2026-04-11T20:43:00Z"
}
- Notes:
- Stats are computed from the existing
cordum:velocity:*Redis sorted sets maintained by the safety kernel. - This endpoint does not change safety-kernel evaluation behavior.
- Stats are computed from the existing
GET /api/v1/policy/bundles
- Response:
{
"bundles": {},
"items": [
{
"id": "secops/demo",
"enabled": true,
"source": "studio",
"author": "...",
"message": "...",
"created_at": "...",
"updated_at": "...",
"version": "...",
"installed_at": "...",
"sha256": "...",
"rule_count": 4
}
],
"updated_at": "..."
}
GET /api/v1/policy/bundles/{id}
- Response:
{
"id": "secops/demo",
"content": "yaml...",
"enabled": true,
"author": "...",
"message": "...",
"created_at": "...",
"updated_at": "..."
}
PUT /api/v1/policy/bundles/{id}
- Constraint: id must start with
secops/ - Request schema (
policyBundleUpsertRequest):
{
"content": "yaml-policy",
"enabled": true,
"author": "secops",
"message": "update rule"
}
- Response:
{
"id": "secops/demo",
"updated_at": "..."
}
POST /api/v1/policy/bundles/{id}/simulate
- Request schema (
policyBundleSimulateRequest):
{
"request": { "topic": "job.default", "tenant": "default" },
"content": "optional-overridden-policy-content"
}
- Response: protobuf-JSON
PolicyCheckResponse
GET /api/v1/policy/bundles/snapshots
- Response:
{
"items": [
{
"id": "2026-02-13T09:00:00Z-abcd1234",
"created_at": "2026-02-13T09:00:00Z",
"note": "before publish"
}
]
}
POST /api/v1/policy/bundles/snapshots
- Request:
{ "note": "checkpoint" }
- Response: full snapshot object (
id,created_at,note,bundles)
GET /api/v1/policy/bundles/snapshots/{id}
- Response: full snapshot object
- Errors:
400,404
DELETE /api/v1/policy/bundles/{id}
- Auth: required + admin
- Deletes a policy bundle by ID.
- Response:
204 No Content - Errors:
400(invalid id),404(not found)
GET /api/v1/policy/output/rules
- Auth: required
- Response: list of output policy rules.
{ "items": [{ "id": "rule-1", "name": "PII filter", "enabled": true, "action": "redact", "pattern": "..." }] }
- Errors: auth errors.
GET /api/v1/policy/output/stats
- Auth: required
- Response: aggregate output policy statistics (quarantine counts, redaction counts, denial counts).
{ "total_checked": 1024, "quarantined": 12, "redacted": 45, "denied": 3 }
- Errors: auth errors.
PUT /api/v1/policy/output/rules/{id}
- Auth: required + admin
- Updates an existing output policy rule.
- Request:
{ "name": "PII filter", "enabled": true, "action": "redact", "pattern": "..." }
- Response: updated rule object.
- Errors:
400(invalid rule),404(not found)
POST /api/v1/policy/publish
- Request schema (
policyPublishRequest):
{
"bundle_ids": ["secops/a", "secops/b"],
"author": "secops",
"message": "publish",
"note": "release"
}
- Response:
{
"snapshot_before": "...",
"snapshot_after": "...",
"published": ["secops/a", "secops/b"]
}
POST /api/v1/policy/rollback
- Request schema (
policyRollbackRequest):
{
"snapshot_id": "snapshot-id",
"author": "secops",
"message": "rollback",
"note": "incident response"
}
- Response:
{
"snapshot_before": "...",
"snapshot_after": "...",
"rollback_to": "snapshot-id"
}
GET /api/v1/policy/audit
- Response:
{ "items": [ { "id": "...", "action": "publish", "created_at": "..." } ] }
Example: policy evaluate
curl -sS -X POST http://localhost:8081/api/v1/policy/evaluate \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-H 'Content-Type: application/json' \
-d '{"topic":"job.default","tenant":"default"}'
Policy governance endpoints
POST /api/v1/policy/replay
- Auth: required + admin
- Description: What-if analysis — replays historical jobs against a candidate policy to preview decision changes before deployment.
- Request:
{
"from": "2026-04-07T00:00:00Z",
"to": "2026-04-14T00:00:00Z",
"filters": {
"tenant": "default",
"topic_pattern": "job\\.cordclaw\\..*",
"original_decision": "DENY"
},
"candidate_content": "version: \"1\"\nrules:\n ...",
"use_current_policy": false,
"max_jobs": 500
}
- Response:
{
"replay_id": "replay-abc123",
"policy_snapshot": "candidate-inline-sha256:...",
"time_range": { "from": "2026-04-07T00:00:00Z", "to": "2026-04-14T00:00:00Z" },
"summary": {
"total_jobs": 312,
"evaluated": 310,
"escalated": 4,
"relaxed": 12,
"unchanged": 294,
"errored": 2
},
"rule_hits": [
{ "rule_id": "cordclaw-deny-destructive", "decision": "DENY", "count": 8 }
],
"changes": [
{
"job_id": "job-xyz",
"topic": "job.cordclaw.exec",
"tenant": "default",
"original_decision": "DENY",
"new_decision": "ALLOW",
"new_rule_id": "cordclaw-allow-safe-exec",
"new_reason": "Safe exec in sandbox",
"direction": "relaxed"
}
],
"warnings": [],
"errors": []
}
- Notes:
- Provide
candidate_content(inline YAML) orcandidate_bundle_id(existing bundle) oruse_current_policy=true. directionisescalated(stricter),relaxed(looser), orunchanged.- Time range maximum: 7 days. Default
max_jobs: 500 (absolute max: 1000). filtersis optional; omit to replay all jobs in the time range.
- Provide
- Errors:
400(invalid time range/policy),403,500
POST /api/v1/policy/analytics
- Auth: required + admin
- Description: Rule-level analytics — hit counts, override rates, approval latency, and daily histograms for policy rules over a time window.
- Request:
{
"from": "2026-04-07T00:00:00Z",
"to": "2026-04-14T00:00:00Z",
"rule_filter": ""
}
- Response:
{
"time_range": { "from": "2026-04-07T00:00:00Z", "to": "2026-04-14T00:00:00Z" },
"rules": [
{
"rule_id": "cordclaw-approve-package-install",
"hit_count": 47,
"approval_count": 47,
"override_count": 38,
"override_rate": 0.809,
"avg_approval_latency_ms": 12400,
"daily_hits": [8, 5, 12, 3, 7, 6, 6]
}
],
"summary": {
"total_rules": 14,
"total_hits": 312,
"total_overrides": 52,
"highest_override_rule": "cordclaw-approve-package-install"
}
}
- Notes:
- Time range maximum: 7 days. Jobs scanned: up to 1000.
rule_filteroptionally restricts results to a single rule ID.override_rateisoverride_count / approval_count(3 decimal places). Rules with zero approvals have rate 0.daily_hitsis a per-day histogram aligned to thefromdate (max 7 entries).highest_override_rulehighlights the rule most frequently overridden by operators — a signal for policy tuning.
- Errors:
400(invalid time range),403,500
9. Config and Schemas
Config
GET /api/v1/config
- Auth: required (admin for
systemscope; admin or operator for all other scopes) - Query:
scope,scope_id,envelope=true|false - Valid scopes:
system,org,team,workflow,step. Unknown scope values return400. - Response:
- default: flat
dataobject - with
envelope=true: full config document (scope,scope_id,data,meta)
- default: flat
- Errors:
400(invalid scope),403(insufficient role or tenant mismatch)
GET /api/v1/config/effective
- Auth: required + admin or operator
- Query:
org_id,team_id,workflow_id,step_id - Response: merged effective config snapshot
- Errors:
403(viewer role denied)
POST /api/v1/config
- Auth: required + admin
- Accepts:
- wrapped document payload (
scope,scope_id,data,meta) - flat payload (auto-wrapped to
system/default)
- wrapped document payload (
- Response:
204
Schemas
POST /api/v1/schemas
- Auth: required + admin
- Request:
{
"id": "schema-id",
"schema": { "type": "object" }
}
- Response:
204
GET /api/v1/schemas
- Query:
limit - Response:
{ "schemas": ["schema-a", "schema-b"] }
GET /api/v1/schemas/{id}
- Response:
{
"id": "schema-id",
"schema": { "type": "object" }
}
DELETE /api/v1/schemas/{id}
- Auth: required + admin
- Response:
204
10. Topics and Worker Credentials
Topics
GET /api/v1/topics
- Auth: required +
admin,operator, orviewer - Response:
{
"items": [
{
"name": "job.sre-investigator.collect.k8s",
"pool": "sre-investigator",
"input_schema_id": "sre-investigator/IncidentContext",
"output_schema_id": "sre-investigator/IncidentResult",
"pack_id": "sre-investigator",
"requires": ["kubectl", "network:egress"],
"risk_tags": ["network"],
"status": "active",
"active_worker_count": 2
}
],
"registry_empty": false
}
Notes:
-
registry_empty=truemeans the canonical topic registry has not been populated yet, so legacy routing may still be the only authority. -
active_worker_countis derived from the latest runtime worker snapshot for the topic's configured pool. A value of0means the topic is still valid but currently degraded. -
Errors:
403,500,503
POST /api/v1/topics
- Auth: required +
admin - Request schema:
{
"name": "job.sre-investigator.collect.k8s",
"pool": "sre-investigator",
"input_schema_id": "sre-investigator/IncidentContext",
"output_schema_id": "sre-investigator/IncidentResult",
"pack_id": "sre-investigator",
"requires": ["kubectl", "network:egress"],
"risk_tags": ["network"],
"status": "active"
}
Rules:
-
namemust be a valid topic name. -
poolmust reference an existing pool unlessstatusisdisabled. -
statusmay beactive,deprecated, ordisabled. Omitted status defaults toactive. -
Response:
201 Createdfor a new topic,200 OKwhen updating an existing registration.
{
"name": "job.sre-investigator.collect.k8s",
"pool": "sre-investigator",
"input_schema_id": "sre-investigator/IncidentContext",
"output_schema_id": "sre-investigator/IncidentResult",
"pack_id": "sre-investigator",
"requires": ["kubectl", "network:egress"],
"risk_tags": ["network"],
"status": "active"
}
- Errors:
400,403,404,503
DELETE /api/v1/topics/{name}
- Auth: required +
admin - Response:
204 - Errors:
400,403,404,500,503
Example: register and list topics
curl -sS -X POST http://localhost:8081/api/v1/topics \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-H 'Content-Type: application/json' \
-d '{
"name":"job.sre-investigator.collect.k8s",
"pool":"sre-investigator",
"input_schema_id":"sre-investigator/IncidentContext",
"output_schema_id":"sre-investigator/IncidentResult",
"pack_id":"sre-investigator",
"requires":["kubectl","network:egress"],
"risk_tags":["network"],
"status":"active"
}'
curl -sS http://localhost:8081/api/v1/topics \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default'
Worker credentials
GET /api/v1/workers/credentials
- Auth: required +
admin - Response:
{
"items": [
{
"worker_id": "external-worker-01",
"allowed_pools": ["sre-investigator"],
"allowed_topics": ["job.sre-investigator.collect.k8s"],
"pack_id": "",
"created_by": "admin",
"created_at": "2026-04-07T12:00:00Z"
}
]
}
Notes:
-
Revoked credentials remain listable and include
revoked_at. -
Pack-managed credentials may include
pack_id; operator-issued credentials usually leave it empty. -
Errors:
403,500,503
POST /api/v1/workers/credentials
- Auth: required +
admin - Request schema:
{
"worker_id": "external-worker-01",
"allowed_pools": ["sre-investigator"],
"allowed_topics": ["job.sre-investigator.collect.k8s"]
}
Rules:
-
worker_idmust be non-empty and must not contain whitespace. -
allowed_poolsmust reference existing pools. -
allowed_topicsmust reference existing registered topics when the topic registry is populated. -
Creating a credential for an existing
worker_idrotates it and returns a fresh token. -
Response:
201 Createdfor a new credential,200 OKwhen rotating an existing credential.
{
"worker_id": "external-worker-01",
"allowed_pools": ["sre-investigator"],
"allowed_topics": ["job.sre-investigator.collect.k8s"],
"pack_id": "",
"created_by": "admin",
"created_at": "2026-04-07T12:00:00Z",
"token": "9f8d4c..."
}
Important:
-
The plaintext
tokenis returned only once at creation/rotation time. Store it immediately. -
Errors:
400,403,404,500,503
DELETE /api/v1/workers/credentials/{worker_id}
- Auth: required +
admin - Response:
204 - Errors:
400,403,404,500,503
Example: issue and revoke a worker credential
curl -sS -X POST http://localhost:8081/api/v1/workers/credentials \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-H 'Content-Type: application/json' \
-d '{
"worker_id":"external-worker-01",
"allowed_pools":["sre-investigator"],
"allowed_topics":["job.sre-investigator.collect.k8s"]
}'
curl -sS -X DELETE http://localhost:8081/api/v1/workers/credentials/external-worker-01 \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default'
10.1 Agent Identities
All endpoints require admin role. Agent identities are first-class resources representing an AI agent's identity, capabilities, and risk profile. One identity can be linked to multiple worker credentials (e.g. dev + prod for the same agent).
POST /api/v1/agents
- Auth: required +
admin - Request schema:
{
"name": "fraud-detector",
"description": "Detects fraudulent transactions",
"owner": "risk-team",
"team": "risk",
"risk_tier": "high",
"allowed_topics": ["job.fraud-detection.process"],
"allowed_pools": ["default"],
"allowed_tools": [],
"data_classifications": ["pii", "financial"]
}
name(required): display nameowner(required): owner identifierrisk_tier(required):low|medium|high|criticalstatus: defaults toactive. Values:active|suspended|revoked- Response:
201+ agent identity JSON with generatedid,created_at,updated_at - Errors:
400,403,503
GET /api/v1/agents
- Auth: required +
admin - Query params:
cursor,limit(default 50, max 200),status,risk_tier,team - Response:
{
"items": [{ "id": "...", "name": "...", "risk_tier": "high", ... }],
"cursor": "..."
}
- Errors:
400,403,500,503
GET /api/v1/agents/{id}
- Auth: required +
admin - Response: agent identity JSON
- Errors:
400,403,404,503
PUT /api/v1/agents/{id}
- Auth: required +
admin - Request: partial update — only provided fields are updated
- Response: updated agent identity JSON
- Errors:
400,403,404,503
DELETE /api/v1/agents/{id}
- Auth: required +
admin - Soft-delete: sets
statustorevoked, preserves the record for audit - Response:
204 - Errors:
400,403,404,500,503
Worker credential linking
When creating a worker credential via POST /api/v1/workers/credentials, pass agent_id to link the credential to an agent identity:
{
"worker_id": "fraud-worker-01",
"allowed_pools": ["default"],
"allowed_topics": ["job.fraud-detection.process"],
"agent_id": "uuid-of-fraud-detector-identity"
}
The agent_id is validated to exist. Linked credentials return agent_id in list/detail responses.
Safety Kernel integration
When a job is submitted with a worker credential linked to an agent identity, the Safety Kernel automatically enriches policy evaluation with agent context. Policy rules can match on agent fields:
- id: critical-agent-approval
match:
topics: ["job.*"]
agent_risk_tiers: ["high", "critical"]
decision: require_approval
reason: "High/critical risk agents require human approval"
- id: pii-agent-restricted
match:
topics: ["job.public.*"]
agent_data_classifications: ["pii"]
decision: deny
reason: "Agents with PII access cannot run public jobs"
11. Packs and Marketplace
All endpoints in this section require admin role.
Packs
GET /api/v1/packs
- Response:
{ "items": [ { "id": "pack.id", "version": "1.0.0", "status": "ACTIVE" } ] }
GET /api/v1/packs/{id}
- Response: full
packRecord
POST /api/v1/packs/install
- Content-Type:
multipart/form-data - Form fields:
bundlefile (.tgz)forcebooleanupgradebooleaninactiveboolean
- Response: installed
packRecord - Errors:
400,403,409,500,503
POST /api/v1/packs/{id}/uninstall
- Request body (optional):
{ "purge": true }
- Response: updated
packRecordwithstatus: DISABLED
POST /api/v1/packs/{id}/verify
- Response:
{
"pack_id": "pack.id",
"results": [
{
"name": "test-name",
"expected": "ALLOW",
"got": "ALLOW",
"reason": "...",
"ok": true
}
]
}
Marketplace
GET /api/v1/marketplace/packs
- Response:
{
"catalogs": [
{
"id": "official",
"title": "Cordum Official",
"url": "https://packs.cordum.io/catalog.json",
"enabled": true,
"updated_at": "...",
"error": ""
}
],
"items": [
{
"id": "pack.id",
"version": "1.2.3",
"title": "...",
"description": "...",
"url": "https://.../pack.tgz",
"sha256": "...",
"catalog_id": "official",
"catalog_title": "Cordum Official",
"capabilities": [],
"requires": [],
"risk_tags": [],
"installed_version": "1.0.0",
"installed_status": "ACTIVE",
"installed_at": "..."
}
],
"fetched_at": "...",
"cached": true
}
POST /api/v1/marketplace/install
- Request schema (
marketplaceInstallRequest):
{
"catalog_id": "official",
"pack_id": "pack.id",
"version": "1.2.3",
"url": "https://.../pack.tgz",
"sha256": "...",
"force": false,
"upgrade": true,
"inactive": false
}
- Response: installed
packRecord - Errors:
400,403,404,500,503
Example: list marketplace packs
curl -sS http://localhost:8081/api/v1/marketplace/packs \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default'
12. Resource Locks
GET /api/v1/locks
- Auth: required
- Query:
resource - Response schema (
lock):
{
"resource": "packs:global",
"mode": "exclusive",
"owners": {
"api-gateway:123": 1
},
"updated_at": "2026-02-13T09:37:17Z",
"expires_at": "2026-02-13T09:38:17Z"
}
- Errors:
400,404,500,503
POST /api/v1/locks/acquire
- Auth: required + admin
- Request (
lockRequest):
{
"resource": "packs:global",
"owner": "api-gateway:123",
"mode": "exclusive",
"ttl_ms": 60000
}
- Response schema (
lock): same asGET /api/v1/locks - Errors:
400,403,409,503
POST /api/v1/locks/release
- Auth: required + admin
- Request: same
lockRequest - Response schema:
{
"lock": {
"resource": "packs:global",
"mode": "exclusive",
"owners": {},
"updated_at": "2026-02-13T09:38:24Z",
"expires_at": "2026-02-13T09:39:24Z"
},
"released": true
}
- Errors:
400,403,409,503
POST /api/v1/locks/renew
- Auth: required + admin
- Request: same
lockRequest - Response schema (
lock): same asGET /api/v1/locks - Errors:
400,403,404,503
13. Health, Status, Workers, Metrics
GET /health
- Auth: public
- Response: plain text
ok
GET /api/v1/status
- Auth: required
- Response schema:
{
"time": "2026-02-13T09:38:23Z",
"uptime_seconds": 4512,
"build": {
"version": "dev",
"commit": "unknown",
"date": "unknown"
},
"nats": {
"connected": true,
"status": "CONNECTED",
"url": "nats://nats:4222"
},
"redis": {
"ok": true,
"error": "dial tcp ... (admin-only when present)"
},
"workers": {
"count": 1
},
"license": {
"plan": "enterprise"
},
"instance_id": "gw-abc123",
"rate_limiter": {
"mode": "redis"
},
"circuit_breakers": {
"input": {
"state": "CLOSED",
"failures": 0,
"fail_threshold": 3,
"cooldown_remaining_ms": 0
},
"output": {
"state": "CLOSED",
"failures": 0,
"fail_threshold": 3,
"cooldown_remaining_ms": 0
}
},
"replicas": {
"api-gateway": [
{"id": "gw-abc123", "service": "api-gateway", "version": "0.2.0", "commit": "abc123", "started_at": "2026-02-20T12:00:00Z"}
],
"scheduler": [
{"id": "sched-def456", "service": "scheduler", "version": "0.2.0", "commit": "def456", "started_at": "2026-02-20T12:00:01Z"}
]
}
}
- Notes:
- Admin-only fields: The following fields are only included when the caller has the
adminrole:nats.url,redis.error,instance_id,rate_limiter,circuit_breakers,input_fail_open_total,ha_env,snapshot_meta,replicas. Non-admin callers receive a reduced response with onlytime,uptime_seconds,build,nats(connected/status only),redis(ok only),workers,pipeline, andlicense. licenseis optional and only present when auth provider exposes license info.circuit_breakersshows input (pre-dispatch safety) and output (post-execution safety) circuit breaker state. Possible states:CLOSED(healthy),OPEN(failures exceeded threshold),UNKNOWN(Redis unavailable).replicasis a map of service name to array of registered instance info. Only present when the instance registry is active (HA mode with Redis).
- Admin-only fields: The following fields are only included when the caller has the
- Errors: auth/tenant middleware errors (
401,403).
GET /api/v1/telemetry/status
- Auth: required + admin
- Response schema:
{
"mode": "anonymous",
"endpoint": "https://telemetry.cordum.io/v1/report",
"last_collected_at": "2026-04-08T22:00:00Z",
"last_reported_at": "2026-04-08T22:00:05Z"
}
- Notes:
modeis controlled byCORDUM_TELEMETRY_MODE(off,local_only,anonymous).endpointis only populated when remote reporting is enabled.
- Errors: auth/tenant middleware errors (
401,403),500.
GET /api/v1/telemetry/inspect
- Auth: required + admin
- Response: the exact last locally stored telemetry payload as JSON, or
nullif nothing has been collected yet. - Errors: auth/tenant middleware errors (
401,403),500.
GET /api/v1/telemetry/export
- Auth: required + admin
- Response: downloadable JSON attachment (
Content-Disposition: attachment; filename="cordum-telemetry.json") containing the exact last locally stored telemetry payload, ornullif nothing has been collected yet. - Errors: auth/tenant middleware errors (
401,403),500.
GET /api/v1/telemetry/usage
- Auth: required + admin
- Response schema:
{
"workers": {
"registered": 2,
"connected": 1
},
"usage": {
"active_jobs": 1,
"active_workflow_runs": 1,
"jobs_last_24h": 12,
"workflow_runs_last_24h": 4,
"schemas": 3,
"policy_bundles": 2
},
"features_enabled": {
"user_auth": true,
"oidc": false,
"output_policy": true
},
"engagement": {
"topics_configured": 5,
"workflows_configured": 3,
"packs_installed": 1,
"user_auth_enabled": true,
"oidc_enabled": false,
"output_policy_enabled": true
},
"limits_hit": {}
}
- Errors: auth/tenant middleware errors (
401,403),500.
POST /api/v1/telemetry/consent
- Auth: required + admin
- Body:
{"mode": "off"}— set telemetry mode (off,local_only,anonymous). Persisted in Redis and applied immediately. - Errors:
400(invalid mode), auth errors.
GET /api/v1/license
- Auth: required
- Response schema:
{
"plan": "community",
"license": {
"mode": "community",
"status": "active",
"plan": "Community",
"features": ["audit", "break_glass_admin"],
"limits": {
"max_workers": 3,
"max_concurrent_jobs": 3,
"requests_per_second": 500,
"audit_retention_days": 7
}
},
"entitlements": { ... },
"rights": null,
"expiry_status": "active"
}
- Notes: Returns current license plan, entitlements, rights, and expiry status. No license = Community tier.
- Errors: auth errors.
POST /api/v1/license/reload
- Auth: required + admin
- Re-reads the license from environment/disk and revalidates. Use after installing a new license file.
- Errors:
500if license cannot be read/parsed.
GET /api/v1/license/usage
- Auth: required + admin
- Response: current usage vs entitlement limits (workers, jobs, workflows, schemas, policies, rate limits).
- Errors: auth errors.
GET /api/v1/workers
- Auth: required + admin
- Response schema: array of
Heartbeatobjects
[
{
"worker_id": "demo-guardrails-worker",
"region": "local",
"type": "cpu",
"cpu_load": 0,
"gpu_utilization": 0,
"active_jobs": 0,
"capabilities": ["demo"],
"pool": "demo-guardrails",
"max_parallel_jobs": 4,
"labels": {
"tenant": "default"
},
"memory_load": 0,
"progress_pct": 0,
"last_memo": "ready"
}
]
- Errors:
403(non-admin), plus auth/tenant middleware errors (401,403).
GET /api/v1/workers/{id}
- Auth: required + admin
- Response: single
Heartbeatobject for the specified worker. - Errors:
404(worker not found),403(non-admin).
GET /api/v1/workers/{id}/jobs
- Auth: required + admin
- Response: list of jobs currently assigned to or recently completed by the worker.
- Errors:
404(worker not found),403(non-admin).
GET /metrics
- Server: metrics listener (
:9092by default), not on main API mux - Response: Prometheus metrics text format
Note: There are no /healthz, /readyz, or /api/v1/system/health routes in current gateway_core.go registration.
14. Memory and Artifacts
GET /api/v1/memory
- Auth: required + admin
- Query:
ptrorkey - Supports
ctx:*,res:*, andmem:*keys - Tenant isolation: all key prefixes enforce tenant checks.
ctx:/res:keys check the job's tenant.mem:run:{id}:*keys check the workflow run's org.mem:{id}:*keys attempt job tenant lookup. Cross-tenant reads return403. - Response includes pointer/key metadata and payload views (
base64, and optional parsedjson)
POST /api/v1/artifacts
- Auth: required
- Request schema (
artifactPutRequest):
{
"content_base64": "...",
"content": "...",
"content_type": "application/json",
"retention": "24h",
"labels": {"tenant_id":"default"}
}
- Response:
{
"artifact_ptr": "redis://artifact:...",
"size_bytes": 123
}
GET /api/v1/artifacts/{ptr}
- Auth: required + tenant constraints
- Response:
{
"artifact_ptr": "redis://artifact:...",
"content_base64": "...",
"metadata": {}
}
15. WebSocket Streaming
GET /api/v1/stream
- Auth: required + admin role
- Upgrade: websocket
- API key via
X-API-Keyor websocket subprotocol (cordum-api-key, <base64url(key)>) - Streams bus events (
sys.job.*,sys.audit.*) filtered by tenant permissions. - Server sends ping frames every 30 seconds by default (
GATEWAY_WS_PING_INTERVAL) and expects pong handling to keep the socket alive. - Credentials are revalidated every 120 seconds; definitive revocation closes the socket with a policy-violation close frame.
GET /api/v1/jobs/{id}/stream
- Auth: required + tenant access to that job
- Upgrade: websocket
- Streams only events for the specified job id.
- Uses the same ping/pong keepalive and credential revalidation behavior as the global stream.
16. MCP Endpoints (HTTP/SSE)
MCP routes are only registered when mcp.enabled=true and mcp.transport=http in config.
GET /mcp/sse
- Auth: MCP auth wrapper (
AuthenticateHTTP+ tenant checks) - Opens SSE stream; returns
X-MCP-Session-ID.
POST /mcp/message
- Auth: MCP auth wrapper (
AuthenticateHTTP+ tenant checks) - Body: JSON-RPC 2.0 message
Example request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": { "protocolVersion": "2024-11-05" }
}
- Response: JSON-RPC 2.0 response (or
202for notifications)
GET /mcp/status
- Auth: MCP auth wrapper
- Response:
{
"running": true,
"connected_clients": 1,
"uptime_seconds": 42,
"transport": "http",
"enabled_tools": 0,
"enabled_resources": 0
}
Example: MCP message call
curl -sS -X POST http://localhost:8081/mcp/message \
-H 'Content-Type: application/json' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'X-Tenant-ID: default' \
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
17. Admin Endpoints
GET /api/v1/admin/locks — List Active Distributed Locks
Returns all active distributed locks held in Redis. This is a read-only diagnostic endpoint for operators to inspect lock state across scheduler, workflow engine, and gateway services.
Auth: Admin role required.
Response:
{
"locks": [
{
"key": "cordum:scheduler:job:job-abc123",
"holder": "scheduler-1a2b3c",
"ttl_remaining_ms": 24500,
"type": "job"
},
{
"key": "cordum:reconciler:default",
"holder": "scheduler-1a2b3c",
"ttl_remaining_ms": 28000,
"type": "reconciler"
}
]
}
Lock types:
| Type | Key prefix | Description |
|---|---|---|
reconciler | cordum:reconciler: | Scheduler reconciliation loop leader lock |
replayer | cordum:replayer: | Replayer leader lock |
job | cordum:scheduler:job: | Per-job dispatch lock |
snapshot | cordum:scheduler:snapshot: | Snapshot writer lock |
dlq_cleanup | cordum:dlq:cleanup | DLQ cleanup leader lock |
workflow_run | cordum:wf:run:lock: | Per-workflow-run execution lock |
delay_poller | cordum:wf:delay:poller | Delay timer poller leader lock |
workflow_reconciler | cordum:workflow-engine:reconciler: | Workflow engine reconciler leader lock |
rate_limit | cordum:rl: | Rate limiter window keys |
jwks_cache | cordum:auth:jwks: | OIDC/JWT key set cache entries |
circuit_breaker | cordum:cb: | Circuit breaker state keys |
marketplace_cache | cordum:cache:marketplace | Marketplace pack list cache |
Notes:
- Results are capped at 500 entries to prevent oversized responses.
- Uses Redis
SCAN(neverKEYS) so it is safe to call in production. - Keys that expire between the scan and the value read are silently skipped.
- A
ttl_remaining_msof0means the key has no expiry or expiry could not be read.
18. Pool Management
All pool management endpoints require admin role.
GET /api/v1/pools
List all worker pools.
Response (200):
{ "items": [{ "name": "gpu-pool", "status": "active", "requires": ["gpu"], "description": "GPU-enabled pool" }] }
GET /api/v1/pools/{name}
Get details for a specific pool.
Response (200):
{ "name": "gpu-pool", "status": "active", "requires": ["gpu"], "description": "GPU-enabled pool", "topics": ["job.ml.train"] }
Errors: 404 (not found)
PUT /api/v1/pools/{name}
Create a new worker pool.
Request body:
{ "requires": ["docker", "gpu"], "description": "GPU-enabled pool" }
Response (201):
{ "name": "gpu-pool", "status": "active", "requires": ["docker", "gpu"], "description": "GPU-enabled pool" }
Errors: 400 (invalid name), 409 (already exists)
PATCH /api/v1/pools/{name}
Update pool configuration. Only provided fields are changed.
Request body:
{ "requires": ["docker"], "description": "updated description" }
Response (200): Updated pool object.
Errors: 400 (invalid config), 404 (not found)
DELETE /api/v1/pools/{name}
Delete a pool. Fails if the pool has active topic mappings unless ?force=true.
Query params: force=true — remove topic mappings and delete.
Response: 204 No Content
Errors: 400 (active topic mappings), 404 (not found)
POST /api/v1/pools/{name}/drain
Start draining a pool. Stops new job routing; in-flight jobs complete normally.
The pool auto-transitions to inactive when all jobs finish or timeout expires.
Request body:
{ "timeout_seconds": 300 }
Response (200):
{ "name": "my-pool", "status": "draining", "drain_started_at": "2026-03-26T10:00:00Z", "drain_timeout_seconds": 300 }
Errors: 400 (not active / already draining), 404 (not found)
PUT /api/v1/pools/{name}/topics/{topic}
Add a topic-to-pool mapping. Idempotent — succeeds if already mapped.
Response: 204 No Content
Errors: 400 (invalid topic format), 404 (pool not found)
DELETE /api/v1/pools/{name}/topics/{topic}
Remove a topic-to-pool mapping. If the topic has no remaining pools, the topic entry is removed entirely.
Response: 204 No Content
Errors: 404 (pool or topic not found)
Example: create pool and assign topic
# Create pool
curl -X PUT https://gateway:8081/api/v1/pools/gpu-batch \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"requires": ["gpu"], "description": "GPU batch processing"}'
# Assign topic
curl -X PUT https://gateway:8081/api/v1/pools/gpu-batch/topics/job.ml.train \
-H "Authorization: Bearer $API_KEY"
# Drain pool
curl -X POST https://gateway:8081/api/v1/pools/gpu-batch/drain \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"timeout_seconds": 600}'
Endpoint Index (Registered Routes)
The following routes are registered in gateway route setup.
| Method | Path |
|---|---|
| GET | /health |
| GET | /api/v1/auth/config |
| GET | /api/v1/auth/sso/oidc/login |
| GET | /api/v1/auth/sso/oidc/callback |
| GET | /api/v1/auth/sso/saml/metadata |
| GET | /api/v1/auth/sso/saml/login |
| POST | /api/v1/auth/sso/saml/acs |
| POST | /api/v1/auth/login |
| GET | /api/v1/auth/session |
| POST | /api/v1/auth/logout |
| POST | /api/v1/auth/password |
| POST | /api/v1/users |
| GET | /api/v1/users |
| PUT | /api/v1/users/{id} |
| DELETE | /api/v1/users/{id} |
| POST | /api/v1/users/{id}/password |
| GET | /api/v1/auth/keys |
| POST | /api/v1/auth/keys |
| DELETE | /api/v1/auth/keys/{id} |
| GET | /api/v1/auth/roles |
| GET | /api/v1/auth/roles/{name} |
| PUT | /api/v1/auth/roles/{name} |
| DELETE | /api/v1/auth/roles/{name} |
| GET | /api/v1/workers |
| GET | /api/v1/workers/{id} |
| GET | /api/v1/workers/{id}/jobs |
| GET | /api/v1/workers/credentials |
| POST | /api/v1/workers/credentials |
| DELETE | /api/v1/workers/credentials/{worker_id} |
| GET | /api/v1/status |
| GET | /api/v1/jobs |
| POST | /api/v1/jobs |
| GET | /api/v1/jobs/{id} |
| GET | /api/v1/jobs/{id}/stream |
| GET | /api/v1/jobs/{id}/decisions |
| POST | /api/v1/jobs/{id}/cancel |
| POST | /api/v1/jobs/{id}/remediate |
| GET | /api/v1/memory |
| POST | /api/v1/artifacts |
| GET | /api/v1/artifacts/{ptr} |
| GET | /api/v1/traces/{id} |
| GET | /api/v1/workflows |
| POST | /api/v1/workflows |
| GET | /api/v1/workflows/{id} |
| DELETE | /api/v1/workflows/{id} |
| POST | /api/v1/workflows/{id}/runs |
| GET | /api/v1/workflows/{id}/runs |
| POST | /api/v1/workflows/{id}/dry-run |
| GET | /api/v1/workflow-runs |
| GET | /api/v1/workflow-runs/{id} |
| GET | /api/v1/workflow-runs/{id}/timeline |
| GET | /api/v1/workflow-runs/{id}/chat |
| POST | /api/v1/workflow-runs/{id}/chat |
| DELETE | /api/v1/workflow-runs/{id} |
| POST | /api/v1/workflow-runs/{id}/rerun |
| GET | /api/v1/config |
| GET | /api/v1/config/effective |
| POST | /api/v1/config |
| GET | /api/v1/topics |
| POST | /api/v1/topics |
| DELETE | /api/v1/topics/{name} |
| GET | /api/v1/packs |
| GET | /api/v1/packs/{id} |
| POST | /api/v1/packs/install |
| POST | /api/v1/packs/{id}/uninstall |
| POST | /api/v1/packs/{id}/verify |
| GET | /api/v1/marketplace/packs |
| POST | /api/v1/marketplace/install |
| POST | /api/v1/schemas |
| GET | /api/v1/schemas |
| GET | /api/v1/schemas/{id} |
| DELETE | /api/v1/schemas/{id} |
| GET | /api/v1/locks |
| POST | /api/v1/locks/acquire |
| POST | /api/v1/locks/release |
| POST | /api/v1/locks/renew |
| GET | /api/v1/dlq |
| GET | /api/v1/dlq/page |
| DELETE | /api/v1/dlq/{job_id} |
| POST | /api/v1/dlq/{job_id}/retry |
| POST | /api/v1/workflows/{id}/runs/{run_id}/cancel |
| GET | /api/v1/approvals |
| POST | /api/v1/approvals/{job_id}/approve |
| POST | /api/v1/approvals/{job_id}/reject |
| POST | /api/v1/approvals/{job_id}/repair |
| POST | /api/v1/policy/evaluate |
| POST | /api/v1/policy/simulate |
| POST | /api/v1/policy/explain |
| GET | /api/v1/policy/snapshots |
| GET | /api/v1/policy/rules |
| GET | /api/v1/policy/velocity-rules |
| POST | /api/v1/policy/velocity-rules |
| GET | /api/v1/policy/velocity-rules/stats |
| PUT | /api/v1/policy/velocity-rules/{id} |
| DELETE | /api/v1/policy/velocity-rules/{id} |
| GET | /api/v1/policy/bundles |
| GET | /api/v1/policy/bundles/{id} |
| PUT | /api/v1/policy/bundles/{id} |
| DELETE | /api/v1/policy/bundles/{id} |
| POST | /api/v1/policy/bundles/{id}/simulate |
| GET | /api/v1/policy/bundles/snapshots |
| POST | /api/v1/policy/bundles/snapshots |
| GET | /api/v1/policy/bundles/snapshots/{id} |
| POST | /api/v1/policy/publish |
| POST | /api/v1/policy/rollback |
| GET | /api/v1/policy/audit |
| GET | /api/v1/policy/output/rules |
| GET | /api/v1/policy/output/stats |
| PUT | /api/v1/policy/output/rules/{id} |
| GET | /api/v1/pools |
| GET | /api/v1/pools/{name} |
| PUT | /api/v1/pools/{name} |
| PATCH | /api/v1/pools/{name} |
| DELETE | /api/v1/pools/{name} |
| POST | /api/v1/pools/{name}/drain |
| PUT | /api/v1/pools/{name}/topics/{topic} |
| DELETE | /api/v1/pools/{name}/topics/{topic} |
| GET | /api/v1/admin/locks |
| GET | /api/v1/stream (websocket upgrade) |
| GET | /mcp/sse |
| POST | /mcp/message |
| GET | /mcp/status |