API Reference
Base URL: http://localhost:8000 (dev) or your production domain.
All management API paths are prefixed /api/v1/. The MCP proxy endpoint is /mcp/v1/{server_id}.
OpenAPI docs (dev only): http://localhost:8000/docs
Authentication
Admin Auth
All /api/v1/ routes (except /api/v1/auth/token and /api/v1/enroll/*) require an admin JWT:
Authorization: Bearer <admin_jwt>
Agent Auth (Proxy)
All /mcp/v1/ proxy routes require an agent JWT:
Authorization: Bearer <agent_jwt>
Health
GET /health
Returns service health.
curl http://localhost:8000/health
{"status": "ok", "service": "behavry", "version": "0.1.0"}
Auth — POST /api/v1/auth/token
Agent authentication (OAuth 2.1 client credentials).
curl -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "c-uuid-...",
"client_secret": "secret-hex...",
"grant_type": "client_credentials"
}'
Response 200:
{
"access_token": "eyJhbGci...",
"token_type": "Bearer",
"expires_in": 3600,
"agent_id": "a-uuid-...",
"risk_tier": "medium"
}
Errors:
401 Unauthorized: invalid client_id or client_secret403 Forbidden: agent suspended or deprovisioned
Auth — POST /api/v1/auth/admin/login
Admin login (password mode).
curl -X POST http://localhost:8000/api/v1/auth/admin/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin"}'
Response 200:
{
"access_token": "eyJhbGci...",
"token_type": "Bearer",
"username": "admin"
}
Agents
GET /api/v1/agents
List all agents.
curl http://localhost:8000/api/v1/agents \
-H "Authorization: Bearer $ADMIN_TOKEN"
Query params: status=active, limit=50, offset=0
Response 200:
{
"agents": [
{
"id": "a-uuid-...",
"name": "filesystem-reader-01",
"agent_type": "autonomous",
"owner": "data-team",
"status": "active",
"risk_score": 32.5,
"risk_tier": "medium",
"client_id": "c-uuid-...",
"created_at": "2025-02-01T10:00:00Z",
"baseline_status": "approved",
"has_recent_drift": false
}
],
"total": 1
}
POST /api/v1/agents
Register a new agent. Returns client_secret once (plaintext).
curl -X POST http://localhost:8000/api/v1/agents \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "filesystem-reader-01",
"agent_type": "autonomous",
"owner": "data-team",
"description": "Reads project files for summarization"
}'
Response 201:
{
"id": "a-uuid-...",
"name": "filesystem-reader-01",
"client_id": "c-uuid-...",
"client_secret": "hex-secret-shown-once",
"status": "active",
"created_at": "2025-02-01T10:00:00Z"
}
GET /api/v1/agents/{id}
Get agent details.
PATCH /api/v1/agents/{id}
Update agent (name, description, status).
curl -X PATCH http://localhost:8000/api/v1/agents/a-uuid-... \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "suspended"}'
POST /api/v1/agents/{id}/rotate
Rotate agent credentials (generates new client_secret).
POST /api/v1/agents/{id}/roles
Assign a role to an agent.
curl -X POST http://localhost:8000/api/v1/agents/a-uuid-.../roles \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"role_id": "r-uuid-..."}'
DELETE /api/v1/agents/{id}/roles/{role_id}
Remove a role from an agent.
Roles
GET /api/v1/roles
List all roles.
POST /api/v1/roles
Create a new role.
curl -X POST http://localhost:8000/api/v1/roles \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "project-file-reader",
"description": "Read-only access to /home/projects/",
"permissions": ["filesystem:read"],
"resource_scopes": ["/home/projects/"]
}'
Policies
GET /api/v1/policies
List all policies. Query: status=active|draft|archived
POST /api/v1/policies
Create a policy.
curl -X POST http://localhost:8000/api/v1/policies \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Read-only Agent Policy",
"description": "Restricts agent to read operations only",
"rego_content": "package behavry.authz.custom\nimport rego.v1\ndecision := {\"result\": \"deny\", \"reason\": \"write ops not permitted\", \"policy\": \"custom.readonly\"} if { input.request.action in {\"write\", \"delete\"} }",
"status": "draft"
}'
PATCH /api/v1/policies/{id}
Update policy. To activate: {"status": "active"} (syncs to OPA).
POST /api/v1/policies/{id}/evaluate
Test a policy against a sample input.
curl -X POST http://localhost:8000/api/v1/policies/{id}/evaluate \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent": {"id": "test", "roles": [], "permissions": [], "risk_tier": "low"},
"request": {"tool_name": "write_file", "action": "write", "resource": "/tmp/test.txt", "parameters": {}, "mcp_server": "filesystem"}
}'
Audit Events
GET /api/v1/audit/events
Query audit events.
Query params:
agent_id— filter by agentpolicy_result—allow|deny|escalatefrom— ISO datetimeto— ISO datetimelimit— default 100, max 1000offset— pagination
curl "http://localhost:8000/api/v1/audit/events?policy_result=deny&limit=20" \
-H "Authorization: Bearer $ADMIN_TOKEN"
GET /api/v1/audit/stream
SSE live stream. Connect with EventSource.
curl -N http://localhost:8000/api/v1/audit/stream \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Accept: text/event-stream"
GET /api/v1/audit/export
Export audit events. Query: format=json|cef, from, to.
Escalations
GET /api/v1/escalations
List escalations. Query: status=pending|approved|denied|timed_out, limit, offset.
curl "http://localhost:8000/api/v1/escalations?status=pending" \
-H "Authorization: Bearer $ADMIN_TOKEN"
POST /api/v1/escalations/{id}/approve
Approve an escalation.
curl -X POST http://localhost:8000/api/v1/escalations/esc-uuid-.../approve \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Confirmed with data team lead", "create_exception": false}'
POST /api/v1/escalations/{id}/deny
Deny an escalation.
curl -X POST http://localhost:8000/api/v1/escalations/esc-uuid-.../deny \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"notes": "Not authorized"}'
Alerts / Monitor
GET /api/v1/monitor/alerts
List alerts. Query: status, severity, agent_id, limit.
PATCH /api/v1/monitor/alerts/{id}
Acknowledge or resolve an alert.
curl -X PATCH http://localhost:8000/api/v1/monitor/alerts/alert-uuid-... \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "resolved", "resolved_by": "admin"}'
Risk
GET /api/v1/risk
Get risk summary for all agents.
POST /api/v1/risk/{agent_id}/score
Trigger a risk score recalculation for an agent.
Policy Exceptions
GET /api/v1/exceptions
List active policy exceptions.
POST /api/v1/exceptions
Create a standing exception.
DELETE /api/v1/exceptions/{id}
Revoke an exception (soft-delete).
PATCH /api/v1/exceptions/{id}/extend
Extend the expiry of a timed exception. Each exception can be extended up to 4 times (configurable via max_extensions). Returns 409 if the maximum has been reached.
curl -X PATCH http://localhost:8000/api/v1/exceptions/{id}/extend \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hours": 24}'
Response includes extension_count and max_extensions fields.
MCP Proxy
POST /mcp/v1/{server_id}
Forward a JSON-RPC 2.0 MCP request through the enforcement engine.
curl -X POST http://localhost:8000/mcp/v1/filesystem \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {"path": "/home/projects/report.pdf"}
}
}'
Response (allowed):
{
"jsonrpc": "2.0",
"id": 1,
"result": {"content": [{"type": "text", "text": "...file content..."}]}
}
Response (denied):
{
"jsonrpc": "2.0",
"id": 1,
"error": {"code": -32003, "message": "Denied by policy: Access to sensitive files is not permitted"}
}
Response (escalated + timed out):
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32004,
"message": "Escalation timed out — action auto-denied",
"data": {"resolution": "timed_out"}
}
}
JSON-RPC Error Codes
| Code | Meaning |
|---|---|
| -32000 | Unauthorized (invalid/expired token) |
| -32003 | Forbidden (policy deny) |
| -32004 | Escalated (timed out or denied after escalation) |
| -32603 | Internal error |
Enrollment
POST /api/v1/admin/enrollment-tokens
Create a one-time enrollment token (admin only).
POST /api/v1/enroll
Self-register an agent using an enrollment token (no admin auth required).
Tenants (Super Admin only)
GET /api/v1/tenants
List all tenants.
POST /api/v1/tenants
Create a new tenant.
DLP Patterns
All routes require admin auth. The scanner hot-reloads after every create/update/delete — no restart needed.
GET /api/v1/dlp/patterns
List all DLP patterns (builtins first, then custom).
Response:
{
"patterns": [
{
"id": "uuid",
"name": "aws_keys",
"pattern_type": "builtin",
"regex": "AKIA[0-9A-Z]{16}",
"severity": "critical",
"enabled": true,
"description": "AWS Access Key IDs",
"created_at": "2026-01-01T00:00:00Z"
}
]
}
POST /api/v1/dlp/patterns
Create a custom DLP pattern. Returns 201 on success, 409 if name already exists, 422 if regex is invalid.
Body:
{
"name": "internal_project_code",
"regex": "\\bPROJ-\\d{4}\\b",
"severity": "medium",
"description": "Internal project identifiers"
}
PATCH /api/v1/dlp/patterns/{pattern_id}
Update a pattern. Builtins: only enabled and severity are mutable. Custom patterns: all fields.
Body (all fields optional):
{
"enabled": false,
"severity": "high",
"name": "new_name",
"regex": "new_regex",
"description": "updated description"
}
DELETE /api/v1/dlp/patterns/{pattern_id}
Delete a custom pattern. Returns 204 on success, 409 if the pattern is a builtin (disable instead).
Browser Events
GET /api/v1/browser/events
List browser extension events. All query params are optional.
| Param | Type | Description |
|---|---|---|
ai_service | string | Filter by AI service hostname (e.g. chatgpt.com) |
dlp_only | bool | Only return events with DLP findings |
dlp_action | string | blocked | warned | logged |
severity | string | low | medium | high | critical |
limit | int | Page size (default 50) |
offset | int | Pagination offset |
GET /api/v1/browser/trends
Daily aggregate of browser event activity over a rolling window.
| Param | Type | Default | Description |
|---|---|---|---|
days | int | 30 | Number of days to include |
Response:
{
"points": [
{
"date": "2026-02-20",
"visits": 142,
"dlp_hits": 18,
"dlp_blocked": 4
}
]
}
GET /api/v1/browser/summary
Per-service aggregate stats used by the Risk Matrix Shadow AI Exposure section.
Response (per service):
{
"services": [
{
"ai_service": "chatgpt.com",
"visit_count": 420,
"dlp_hits": 34,
"dlp_blocked": 8,
"distinct_users": 12,
"top_users": ["alice@corp.com", "bob@corp.com"]
}
]
}
Sessions
GET /api/v1/sessions
List agent sessions with aggregated metrics (tool call count, DLP hits, risk score at close). Requires admin JWT.
Query params:
status— filter by session status (e.g.active,expired)agent_id— filter by agentoffset— pagination offset (default 0)limit— page size (default 50, max 200)
Response 200:
{
"sessions": [
{
"id": "sess-uuid-...",
"agent_id": "a-uuid-...",
"agent_name": "filesystem-reader-01",
"started_at": "2026-03-01T10:00:00Z",
"expires_at": "2026-03-01T11:00:00Z",
"status": "active",
"tool_call_count": 42,
"dlp_hit_count": 3,
"risk_score_at_close": 28.5,
"last_event_at": "2026-03-01T10:45:00Z"
}
],
"total": 1
}
GET /api/v1/sessions/{id}/events
Event timeline for a session. Returns chronologically ordered audit events.
Query params: offset (default 0), limit (default 100, max 500).
Response 200:
{
"events": [
{
"id": "evt-uuid-...",
"timestamp": "2026-03-01T10:05:00Z",
"action": "read_file",
"target": "/home/projects/report.pdf",
"tool_name": "read_file",
"mcp_server": "filesystem",
"policy_result": "allow",
"policy_reason": null,
"behavioral_score": 12.0,
"dlp_action": null,
"dlp_findings_count": 0
}
],
"total": 42
}
Errors: 404 if session not found or does not belong to the tenant.
Workflows
Manage multi-agent workflows, sessions, and decision traces.
POST /api/v1/workflows
Create a workflow definition. Requires admin JWT.
curl -X POST http://localhost:8000/api/v1/workflows \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "data-pipeline",
"owner_agent_id": "a-uuid-...",
"max_depth": 3,
"max_participants": 5,
"participants": [
{"agent_id": "a-uuid-...", "role": "orchestrator", "allowed_actions": ["read", "write"]}
]
}'
GET /api/v1/workflows
List workflows. Query: status_filter, offset, limit.
GET /api/v1/workflows/{id}
Get a workflow by ID.
PUT /api/v1/workflows/{id}
Update a workflow definition (name, participants, max_depth, etc.).
POST /api/v1/workflows/{id}/suspend
Suspend a workflow. All active sessions become non-startable.
DELETE /api/v1/workflows/{id}
Delete a workflow. Returns 204.
POST /api/v1/workflows/{id}/sessions
Start a new workflow session. Returns a wf_token JWT for agents to pass in X-Workflow-Session headers.
Response 201:
{
"id": "ws-uuid-...",
"workflow_id": "wf-uuid-...",
"initiated_by": "a-uuid-...",
"status": "active",
"started_at": "2026-03-01T10:00:00Z",
"completed_at": null,
"metadata": {},
"wf_token": "eyJhbGci...",
"expires_at": "2026-03-01T11:00:00Z"
}
GET /api/v1/workflows/{id}/sessions
List sessions in a workflow. Query: offset, limit.
GET /api/v1/workflows/{id}/sessions/{sid}
Get a specific workflow session.
POST /api/v1/workflows/{id}/sessions/{sid}/complete
Mark a workflow session as completed.
POST /api/v1/workflows/{id}/sessions/{sid}/abort
Abort a workflow session.
GET /api/v1/workflows/{id}/sessions/{sid}/trace
Return the complete decision trace for a workflow session. Builds a causal tree from audit events using parent_event_id and causal_depth. Includes per-agent policy summaries.
Response 200:
{
"session_id": "ws-uuid-...",
"workflow_id": "wf-uuid-...",
"nodes": [
{
"event_id": "evt-uuid-...",
"agent_id": "a-uuid-...",
"action": "read_file",
"policy_result": "allow",
"causal_depth": 0,
"parent_event_id": null,
"children": []
}
],
"agent_summaries": {}
}
GET /api/v1/workflows/{id}/sessions/{sid}/trace/export
Export the decision trace as a downloadable JSON file. Returns a Content-Disposition: attachment response.
GET /api/v1/workflows/{id}/sessions/{sid}/delegations
List all delegations issued within a workflow session.
Response 200:
{
"delegations": [
{
"id": "del-uuid-...",
"workflow_session_id": "ws-uuid-...",
"delegator_agent_id": "a-uuid-...",
"delegatee_agent_id": "a-uuid-...",
"delegation_depth": 1,
"parent_delegation_id": null,
"effective_permissions": {"tools": ["read_file"], "resources": ["/home/projects/*"]},
"reason": "Sub-task file read",
"status": "active",
"created_at": "2026-03-01T10:05:00Z",
"expires_at": "2026-03-01T11:05:00Z",
"revoked_at": null
}
],
"total": 1
}
Delegations
Issue, inspect, and revoke scoped delegation tokens for multi-agent workflows. All routes require admin JWT.
POST /api/v1/delegations
Issue a scoped delegation token (d_token). Validates that the requested scope is a subset of the delegator's effective permissions and that the delegation depth does not exceed the session's max_depth.
curl -X POST http://localhost:8000/api/v1/delegations \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workflow_session_id": "ws-uuid-...",
"delegator_agent_id": "a-uuid-...",
"delegatee_agent_id": "a-uuid-...",
"scope": {"tools": ["read_file"], "resources": ["/home/projects/*"]},
"reason": "Delegating file read for sub-task"
}'
Response 201:
{
"id": "del-uuid-...",
"workflow_session_id": "ws-uuid-...",
"delegator_agent_id": "a-uuid-...",
"delegatee_agent_id": "a-uuid-...",
"delegation_depth": 1,
"effective_permissions": {"tools": ["read_file"], "resources": ["/home/projects/*"]},
"status": "active",
"d_token": "eyJhbGci...",
"created_at": "2026-03-01T10:05:00Z",
"expires_at": "2026-03-01T11:05:00Z"
}
The d_token JWT is shown only in the creation response. Store it securely.
Errors:
403 SCOPE_EXCEEDS_DELEGATOR: requested scope exceeds delegator's permissions403 DEPTH_EXCEEDS_MAX: delegation depth exceeds workflow max_depth
GET /api/v1/delegations/{id}
Retrieve a delegation record by ID.
POST /api/v1/delegations/{id}/revoke
Revoke an active delegation immediately. The corresponding d_token will be rejected at the proxy even before its JWT expiry because the DB row is checked at enforcement time.
Policy Candidates (Red Team Automation)
Auto-generated candidate Rego policies proposed by the Red Team Automation Loop. All routes require admin JWT.
GET /api/v1/policy-candidates
List policy candidates with optional filters.
Query params:
status—proposed,approved,rejected,auto_activatedsource_event_type— filter by originating event typemin_confidence/max_confidence— confidence score range (0.0-1.0)offset,limit
Response 200:
{
"items": [
{
"id": "pc-uuid-...",
"tenant_id": "t-uuid-...",
"source_event_type": "INBOUND_INJECTION_DETECTED",
"pattern_signature": "injection_block:read_file:high",
"rego_rule": "package behavry.authz.autogen\n...",
"rego_package": "behavry.authz.autogen",
"confidence": 0.75,
"status": "proposed",
"review_notes": null,
"activated_policy_id": null,
"reviewed_by": null,
"reviewed_at": null,
"created_at": "2026-03-15T10:00:00Z",
"updated_at": "2026-03-15T10:00:00Z"
}
],
"total": 5,
"offset": 0,
"limit": 50
}
GET /api/v1/policy-candidates/stats
Aggregated statistics for policy candidates in a trailing window.
Query params: window_days (default 30).
Response 200:
{
"proposed": 12,
"approved": 5,
"rejected": 3,
"auto_activated": 2,
"window_days": 30
}
GET /api/v1/policy-candidates/{id}
Get a single policy candidate by ID.
POST /api/v1/policy-candidates/{id}/approve
Approve a candidate: creates and activates a Policy in OPA.
Body:
{
"reviewed_by": "admin",
"review_notes": "Verified against test scenarios"
}
Errors: 409 if candidate is not in proposed status.
POST /api/v1/policy-candidates/{id}/reject
Reject a candidate with optional notes.
Body:
{
"reviewed_by": "admin",
"review_notes": "False positive — pattern too broad"
}
PATCH /api/v1/policy-candidates/{id}/rego
Edit the generated Rego rule before approval. Only allowed on proposed candidates.
Body:
{
"rego_rule": "package behavry.authz.autogen\nimport rego.v1\n..."
}
Errors: 409 if candidate is not in proposed status.
POST /api/v1/policy-candidates/{id}/test
Dry-run the candidate Rego rule against sample input via OPA.
Body:
{
"input": {
"agent": {"id": "test", "risk_tier": "medium"},
"request": {"tool_name": "read_file", "action": "read"}
}
}
Response 200:
{
"candidate_id": "pc-uuid-...",
"opa_result": {"result": {}},
"rego_package": "behavry.authz.autogen"
}
Policy Change Requests
Submit, review, approve, or reject proposed changes to existing Rego policies. All routes require admin JWT.
GET /api/v1/policies/change-requests/pending
Return the count of all pending change requests across all policies.
Response 200:
{
"total": 3
}
POST /api/v1/policies/{id}/change-requests
Submit a change request for a policy.
Body:
{
"proposed_rego_content": "package behavry.authz.custom\nimport rego.v1\n...",
"proposed_name": "Updated Filesystem Policy",
"proposed_description": "Added exception for /tmp directory",
"summary": "Allow read access to /tmp for low-risk agents"
}
Response 201: Returns the created change request with status: "pending".
Errors: 404 if the policy does not exist.
GET /api/v1/policies/{id}/change-requests
List change requests for a policy. Query: req_status (optional filter).
POST /api/v1/policies/{id}/change-requests/{req_id}/approve
Approve a change request. Applies the proposed Rego content to the policy and re-syncs OPA. Writes an audit log entry with action policy_change_approved.
Body:
{
"notes": "Reviewed and confirmed safe"
}
POST /api/v1/policies/{id}/change-requests/{req_id}/reject
Reject a change request. Writes an audit log entry with action policy_change_rejected.
Body:
{
"notes": "Scope too broad — needs tighter resource constraint"
}
SIEM Destinations
Configure, test, and manage SIEM delivery destinations. All routes require admin JWT. Credentials are AES-256-GCM encrypted at rest and never returned in responses.
POST /api/v1/siem/destinations
Create a new SIEM destination.
Body:
{
"name": "Splunk Production",
"destination_type": "splunk",
"format": "json",
"endpoint_url": "https://splunk.corp.com:8088/services/collector",
"credential": "splunk-hec-token-...",
"event_filter": {"event_types": ["TOOL_CALL", "INBOUND_INJECTION_BLOCKED"]},
"batch_size": 100,
"flush_interval_secs": 30,
"retry_max_attempts": 5,
"retry_backoff_secs": 10,
"enabled": true
}
Supported destination_type values: splunk, sentinel, chronicle, qradar, syslog, webhook.
Supported format values: json, cef, leef.
GET /api/v1/siem/destinations
List all SIEM destinations for the current tenant.
GET /api/v1/siem/destinations/{id}
Get a destination by ID. Credential is never returned; only a hint is included.
PATCH /api/v1/siem/destinations/{id}
Update a destination. Providing a new credential re-encrypts it. Only supplied fields are updated.
DELETE /api/v1/siem/destinations/{id}
Soft-delete a destination. Disables delivery but retains DLQ entries. Returns 204.
POST /api/v1/siem/destinations/{id}/test
Send a synthetic test event to verify connectivity.
Response 200:
{
"delivered": true,
"latency_ms": 142.5,
"error": null
}
GET /api/v1/siem/destinations/{id}/health
Get health statistics for a destination.
Response 200:
{
"destination_id": "dest-uuid-...",
"last_delivery_at": "2026-03-15T10:30:00Z",
"consecutive_failures": 0,
"last_error": null,
"enabled": true,
"dlq_depth": 0
}
POST /api/v1/siem/destinations/{id}/retry-dlq
Re-queue all unresolved DLQ entries for a destination.
Response 200:
{
"queued": 15,
"message": "Re-queued 15 events from 3 DLQ entries"
}
GET /api/v1/siem/dlq
List dead-letter queue entries. Query: destination_id (optional filter).
Response 200:
{
"items": [
{
"id": "dlq-uuid-...",
"destination_id": "dest-uuid-...",
"tenant_id": "t-uuid-...",
"batch_id": "batch-...",
"event_ids": ["evt-1", "evt-2"],
"attempt_count": 3,
"last_attempt_at": "2026-03-15T10:00:00Z",
"next_retry_at": "2026-03-15T10:10:00Z",
"error_message": "Connection refused",
"resolved": false,
"created_at": "2026-03-15T09:50:00Z"
}
],
"total": 1
}
POST /api/v1/siem/dlq/{id}/discard
Mark a DLQ entry as resolved without retrying.
Data Protection
Manage the tenant's data protection pipeline: payload mode, redaction, encryption, and retention. All routes require admin JWT.
GET /api/v1/admin/data-protection
Return the current data protection policy for this tenant.
Response 200:
{
"payload_mode": "redacted",
"redact_dlp_matches": true,
"redact_fields": ["password", "secret", "token"],
"hash_identifiers": true,
"encryption_enabled": false,
"kms_provider": null,
"kms_key_id_suffix": null,
"payload_retention_days": 90,
"strip_payload_from_stream": true
}
Supported payload_mode values: full, metadata_only, redacted, encrypted.
PATCH /api/v1/admin/data-protection
Update the data protection policy. Only provided fields are changed.
Body (all fields optional):
{
"payload_mode": "encrypted",
"redact_dlp_matches": true,
"encryption_enabled": true,
"kms_provider": "local",
"kms_key_id": "my-key-id",
"payload_retention_days": 30,
"test_kms_connectivity": true
}
Set test_kms_connectivity: true to verify the KMS provider is reachable before saving. Returns 400 if the connectivity test fails.
Errors: 400 if encryption_enabled is true but kms_provider is not set.
GET /api/v1/audit/retention-status
Return payload retention metrics for the compliance dashboard.
Response 200:
{
"events_with_payload": 15420,
"purged_last_24h": 320
}
POST /api/v1/audit/events/{event_id}/decrypt
Decrypt the payload of an encrypted audit event. Writes an immutable PAYLOAD_DECRYPTED audit log entry regardless of success or failure.
Response 200:
{
"event_id": "evt-uuid-...",
"decrypted": {"request": {"tool_name": "read_file", "arguments": {"path": "/tmp/data.csv"}}}
}
Errors:
400if the event payload is not encrypted404if the event is not found500if decryption fails (KMS error)
Blast Radius
Configure per-tenant action blast radius thresholds. Used by the proxy engine (step 4d) to deny or escalate high-impact operations before OPA evaluation. All routes require admin JWT.
GET /api/v1/admin/blast-radius
Get the current blast radius thresholds. Returns defaults if no custom config exists.
Response 200:
{
"email_recipient_limit": 50,
"min_delete_depth": 2,
"bulk_action_threshold": 100,
"config_path_prefixes": ["/etc/", "/usr/local/etc/"],
"protected_file_patterns": ["*.pem", "*.key", "*.env"],
"is_custom": false
}
PATCH /api/v1/admin/blast-radius
Update blast radius thresholds. Only provided fields are changed; others retain their current values. Syncs updated config to OPA.
Body (all fields optional):
{
"email_recipient_limit": 25,
"min_delete_depth": 3,
"bulk_action_threshold": 50,
"config_path_prefixes": ["/etc/", "/opt/config/"],
"protected_file_patterns": ["*.pem", "*.key", "*.env", "*.cert"]
}
Errors: 400 if no tenant context, 404 if tenant config not found.
Discovery (AI Asset Discovery)
Discover and inventory AI platforms in use across the organization. Includes connector management (IdP and SaaS integrations) and platform governance tracking. All routes require admin JWT.
Connectors
POST /api/v1/discovery/connectors
Create a discovery connector (e.g. Okta, Azure AD, Google, or SaaS admin API).
Body:
{
"connector_type": "okta",
"label": "Okta Production",
"credentials": {"api_token": "00abc..."},
"config": {"domain": "corp.okta.com"},
"sync_interval_hours": 12
}
Supported connector_type values: okta, azure_ad, google, m365, github, slack, salesforce, atlassian, servicenow, zendesk.
GET /api/v1/discovery/connectors
List all connectors for the current tenant.
DELETE /api/v1/discovery/connectors/{id}
Delete a connector. Returns 204.
POST /api/v1/discovery/connectors/{id}/sync
Trigger an immediate sync for a connector. Returns 202 Accepted (runs in background).
Response 202:
{
"status": "accepted",
"connector_id": "conn-uuid-..."
}
POST /api/v1/discovery/connectors/{id}/test
Test connector connectivity without running a full sync.
Response 200:
{
"success": true,
"error": null,
"sample_data": {}
}
Platforms
GET /api/v1/discovery/platforms
List discovered AI platforms. All query params are optional.
| Param | Type | Description |
|---|---|---|
state | string | Filter by platform state |
risk_tier | string | Filter by risk tier |
capability_class | string | Filter by capability class |
ungoverned_only | bool | Only show ungoverned platforms |
vendor | string | Filter by vendor name |
GET /api/v1/discovery/platforms/{id}
Get detailed information about a discovered platform.
PATCH /api/v1/discovery/platforms/{id}
Update governance metadata for a platform (suppress, link to governed agent, add notes).
Body (all fields optional):
{
"suppressed": true,
"suppression_note": "Internal tool, not customer-facing",
"governed_agent_id": "a-uuid-..."
}
Summary
GET /api/v1/discovery/summary
Get a high-level discovery summary including exposure score and findings breakdown. Cached per tenant for 60 seconds.
Response 200:
{
"platforms_discovered": 14,
"enabled": 10,
"governed": 6,
"coverage_gap": 4,
"exposure_score": 42,
"last_sync": "2026-03-15T10:00:00Z",
"findings_breakdown": {
"telemetry_observed": 8,
"likely_ai_related": 12,
"strongly_indicative": 5,
"operator_confirmed": 3,
"ungoverned_total": 7
}
}
Data Plane Tokens (Hybrid Deployment)
Manage data-plane registration tokens for hybrid (control-plane / data-plane) deployments. Token endpoints require admin JWT; heartbeat uses Bearer <dp_token> auth.
POST /api/v1/admin/data-plane-tokens
Issue a new data-plane token for bundle polling, JWKS fetch, and heartbeat.
Body:
{
"label": "us-east-1 data plane",
"region": "us-east-1",
"expires_days": 365
}
Response 200:
{
"id": "dpt-uuid-...",
"label": "us-east-1 data plane",
"tenant_id": "t-uuid-...",
"deployment_id": null,
"region": "us-east-1",
"created_at": "2026-03-15T10:00:00Z",
"expires_at": "2027-03-15T10:00:00Z",
"token": "dp_abc123..."
}
The token plaintext is shown only once. Store it as BEHAVRY_CP_TOKEN on the data-plane.
Errors: 403 if caller is super-admin (must be a tenant admin).
GET /api/v1/admin/data-plane-tokens
List all data-plane tokens for the current tenant. Token plaintext is never returned.
DELETE /api/v1/admin/data-plane-tokens/{id}
Revoke a data-plane token (sets revoked_at). OPA bundle polling will receive 401 immediately. Returns 204.
POST /api/v1/data-planes/{deployment_id}/heartbeat
Receive a heartbeat from a data-plane deployment. Authenticated via Authorization: Bearer <dp_token> (not admin JWT).
Body:
{
"deployment_id": "dp-us-east-1",
"active_agent_count": 12,
"proxy_rpm_1m": 450.5,
"escalations_pending": 2,
"opa_bundle_version": "2026-03-15T10:00:00Z",
"uptime_seconds": 86400
}
Response 200:
{
"received": true,
"timestamp": "2026-03-15T10:01:00Z"
}
Errors: 401 if token is missing, invalid, revoked, or expired.
Inbound Detection (Source Rules)
Manage trust/block/untrusted rules for inbound injection scanning. Trusted sources skip the scanner; blocked sources receive an immediate substitution with no HITL hold; untrusted sources are scanned with severity promotion. All routes require admin JWT.
GET /api/v1/inbound/rules
List all active inbound source rules for the tenant. Expired rules are excluded.
Response 200:
[
{
"id": "rule-uuid-...",
"tenant_id": "t-uuid-...",
"source_pattern": "github.com/*",
"rule_type": "trust",
"expires_at": null,
"created_by": "admin",
"notes": "Trusted code hosting provider",
"created_at": "2026-03-01T10:00:00Z"
}
]
POST /api/v1/inbound/rules
Create a trust, untrusted, or block rule for an inbound source.
Body:
{
"source_pattern": "unknown-api.example.com/*",
"rule_type": "block",
"expires_in": "7d",
"notes": "Suspicious injection source detected"
}
| Field | Type | Description |
|---|---|---|
source_pattern | string | Pattern to match against inbound sources |
rule_type | string | trust, untrusted, or block |
expires_in | string | session (1h), 24h, 7d, permanent, or null |
notes | string | Optional human-readable note |
Errors: 422 if rule_type is not one of the accepted values or expires_in is unknown.
DELETE /api/v1/inbound/rules/{id}
Delete an inbound source rule. Returns 204.
Errors: 404 if rule not found.
Error Format
All errors follow RFC 7807 Problem Details pattern:
{
"detail": "Agent not found"
}
HTTP status codes: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Validation Error, 500 Internal Server Error.