Skip to main content

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_secret
  • 403 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 agent
  • policy_resultallow | deny | escalate
  • from — ISO datetime
  • to — ISO datetime
  • limit — default 100, max 1000
  • offset — 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

CodeMeaning
-32000Unauthorized (invalid/expired token)
-32003Forbidden (policy deny)
-32004Escalated (timed out or denied after escalation)
-32603Internal 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.

ParamTypeDescription
ai_servicestringFilter by AI service hostname (e.g. chatgpt.com)
dlp_onlyboolOnly return events with DLP findings
dlp_actionstringblocked | warned | logged
severitystringlow | medium | high | critical
limitintPage size (default 50)
offsetintPagination offset

GET /api/v1/browser/trends

Daily aggregate of browser event activity over a rolling window.

ParamTypeDefaultDescription
daysint30Number 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 agent
  • offset — 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 permissions
  • 403 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:

  • statusproposed, approved, rejected, auto_activated
  • source_event_type — filter by originating event type
  • min_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:

  • 400 if the event payload is not encrypted
  • 404 if the event is not found
  • 500 if 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.

ParamTypeDescription
statestringFilter by platform state
risk_tierstringFilter by risk tier
capability_classstringFilter by capability class
ungoverned_onlyboolOnly show ungoverned platforms
vendorstringFilter 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"
}
FieldTypeDescription
source_patternstringPattern to match against inbound sources
rule_typestringtrust, untrusted, or block
expires_instringsession (1h), 24h, 7d, permanent, or null
notesstringOptional 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.