Skip to main content

Tenant Policy API

Use the tenant policy API to create per-tenant governance rules on /api/v1/dynamic-policies. These policies live on the Orchestrator and evaluate request-time context such as user attributes, estimated risk, cost, business rules, and media-governance conditions.

Not yet in the generated reference

The /api/v1/dynamic-policies endpoints on this page are not yet part of the published OpenAPI specs, so this page remains their reference documentation. The closest generated reference is the Orchestrator API reference, which covers the related /api/v1/policies family and a read-only listDynamicPolicies operation at /api/v1/policies/dynamic.

Overview

This is the public API path for what the platform historically called "dynamic policies." In public docs, the preferred term is tenant policies. The legacy endpoint name remains /api/v1/dynamic-policies.

Tenant policies differ from system policies in a few important ways:

FeatureSystem PoliciesTenant Policies
API Endpoint/api/v1/static-policies/api/v1/dynamic-policies
StructurePattern-based rulesConditions and actions
LocationAgentOrchestrator
Use CasePII, SQLi, redaction, fixed governance checksBusiness logic, risk, cost, user and workflow controls
ScopeRequest contentUser, context, cost, risk

Base URL:

http://localhost:8080

The Agent commonly proxies this to the Orchestrator. Direct Orchestrator access also works on 8081 if you expose it intentionally.

Authentication and tenant scope:

  • Via the Agent (port 8080): Authorization: Basic base64(clientId:clientSecret). The Agent authenticates the request and injects the derived X-Tenant-ID / X-Org-ID / X-Client-ID headers before proxying, overwriting any client-supplied values.
  • Direct Orchestrator access (port 8081): the handler reads tenant scope from the X-Tenant-ID header, falling back to X-Org-ID for backward compatibility. Requests with neither return 401 with code UNAUTHORIZED.
  • X-User-ID recommended on mutating requests for audit attribution (recorded as created_by / updated_by).

Errors use the shape {"error": {"code": "<STRING_CODE>", "message": "..."}} (for example UNAUTHORIZED, VALIDATION_ERROR, NOT_FOUND, INTERNAL_ERROR). Field-level validation failures return 400 with code VALIDATION_ERROR and a fields array of {field, message} entries.

Verified Routes

MethodPathPurpose
GET/api/v1/dynamic-policiesList tenant policies
POST/api/v1/dynamic-policiesCreate a tenant policy
POST/api/v1/dynamic-policies/importImport policies
GET/api/v1/dynamic-policies/exportExport policies
GET/api/v1/dynamic-policies/effectiveGet effective policies for a tenant
GET/api/v1/dynamic-policies/{id}Get a single policy
PUT/api/v1/dynamic-policies/{id}Update a policy
DELETE/api/v1/dynamic-policies/{id}Delete a policy
POST/api/v1/dynamic-policies/{id}/testTest a policy
GET/api/v1/dynamic-policies/{id}/versionsList policy versions

Listing and Filtering

Verified query parameters handled by the list endpoint:

Query paramNotes
typeFree-form type filter used by the service
categoryMust start with dynamic- or media-
searchSearch term
sort_bySort field
sort_dirSort direction
pagePage number, default 1
limitPreferred page size, max 100
page_sizeDeprecated alias for backward compatibility
enabledtrue or false

The category behavior matters for real deployments:

  • use dynamic-* for general tenant governance
  • use media-* for media governance and analyzer-driven rules
  • any other prefix is rejected with 400 (VALIDATION_ERROR)
  • type=media without an explicit category automatically filters to media-* categories

Example:

curl "http://localhost:8080/api/v1/dynamic-policies?category=dynamic-risk&enabled=true&limit=20" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \

List responses return a policies array plus pagination. Each policy resource includes:

FieldNotes
idPolicy ID
nameDisplay name
descriptionOptional description
typecontent, user, risk, cost, or other supported policy types
categoryMust use a valid dynamic-* or media-* prefix
tiersystem, organization, or tenant
conditionsCondition list
actionsAction list
priorityEvaluation priority
enabledWhether the policy is active
versionCurrent version
tenant_idTenant scope
organization_idPresent for organization-tier policies
tagsOptional tags
created_at, updated_atTimestamps
created_by, updated_byAudit attribution
deleted_atPresent on soft-deleted records when included

Creating a Tenant Policy

The create handler requires a category and validates that it begins with dynamic- or media-.

Validation rules (violations return 400 with code VALIDATION_ERROR):

  • name - required, 3 to 100 characters
  • description - optional, at most 500 characters
  • type - required, one of content, user, risk, cost, context_aware, media, rate-limit, budget, time-access, role-access, mcp, connector
  • conditions - at least one; each needs a non-empty field and an operator from equals, not_equals, contains, not_contains, contains_any, regex, greater_than, less_than, in, not_in (a regex operator's value must be a string and must compile)
  • actions - at least one; each type must be one of alert, block, log, modify_risk, redact, require_approval, route, warn
  • priority - 0 to 1000

Tier and licensing rules (violations return 403 with a tier-specific error code):

  • tier defaults to tenant; system cannot be created via the API
  • tier: organization requires an Evaluation or Enterprise license (Evaluation enforces an organization-policy limit)
  • Community/free tiers enforce a per-tenant policy count limit
  • Conditions on retry-aware step.* fields require an Evaluation or Enterprise license
Conditions on update, and the empty-array behavior change

On PUT /api/v1/dynamic-policies/{id}, omitting conditions leaves the stored list unchanged. Sending an explicitly empty conditions: [] is rejected with 400 VALIDATION_ERROR, matching create.

Earlier releases accepted conditions: [] on update, so a database can hold policies in a shape no current API can author. From the next platform release those rows are excluded from evaluation on every plane and from the MCP policy listings, and each exclusion increments axonflow_policy_condition_unevaluable_total{reason="empty_conditions",plane}. They stay visible in this API's list and get responses so you can remediate them, and POST /{id}/test reports the exclusion rather than reporting a match.

A stored JSON null conditions value is a different shape with the opposite meaning: it applies to everything. That shape is used by platform-seeded policies and is not creatable through this API. See Managing Policies: a policy with no conditions.

Minimal example:

curl -X POST http://localhost:8080/api/v1/dynamic-policies \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \
-H "X-User-ID: [email protected]" \
-d '{
"name": "High-cost research requests",
"type": "cost",
"category": "dynamic-cost",
"conditions": [
{
"field": "cost_estimate",
"operator": "greater_than",
"value": 5
}
],
"actions": [
{
"type": "block",
"config": {
"message": "Request exceeds the tenant budget threshold"
}
}
],
"enabled": true
}'

Successful creates return 201 Created with a policy object. Validation failures return a structured error with code VALIDATION_ERROR. enabled defaults to false when omitted.

Example response:

{
"policy": {
"id": "550e8400-e29b-41d4-a716-446655440002",
"name": "High-cost research requests",
"type": "cost",
"category": "dynamic-cost",
"tier": "tenant",
"conditions": [
{
"field": "cost_estimate",
"operator": "greater_than",
"value": 5
}
],
"actions": [
{
"type": "block",
"config": {
"message": "Request exceeds the tenant budget threshold"
}
}
],
"priority": 0,
"enabled": true,
"version": 1,
"tenant_id": "tenant-123",
"created_by": "[email protected]",
"created_at": "2026-03-29T12:00:00Z",
"updated_at": "2026-03-29T12:00:00Z"
}
}

Test, Export, Import, and Version History

These are the routes teams use once tenant policy programs become operational rather than experimental:

MethodPathWhy it matters
POST/api/v1/dynamic-policies/{id}/testValidate a rule before enabling it
GET/api/v1/dynamic-policies/exportExport tenant policies for review or migration
POST/api/v1/dynamic-policies/importImport policy sets with overwrite control
GET/api/v1/dynamic-policies/{id}/versionsInspect policy history
GET/api/v1/dynamic-policies/effectiveSee the effective tenant policy set

Test a policy

POST /api/v1/dynamic-policies/{id}/test evaluates the policy's conditions against a simulated request. Request body:

FieldNotes
queryRequired - the simulated request text (400 VALIDATION_ERROR when empty)
userOptional object of user attributes (email, role, department, ...)
request_typeOptional request type string
contextOptional additional context object

Response:

{
"matched": true,
"blocked": true,
"actions": [
{
"type": "block",
"config": {
"message": "Request exceeds the tenant budget threshold"
}
}
],
"explanation": "The policy matched cost_estimate > 5",
"eval_time_ms": 0.42
}

The preview compares fields to values with the same operator semantics the enforcement planes use; what differs is that the fields resolve from the simulated request in the body rather than from live traffic. A policy stored with an explicitly empty conditions list returns matched: false, blocked: false, and an explanation naming the exclusion and the remediation, because no engine would enforce it. See Operator semantics for the comparison rules the preview and the engines share.

Version history

GET /api/v1/dynamic-policies/{id}/versions returns:

{
"versions": [
{
"version": 1,
"snapshot": {
"name": "High-cost research requests"
},
"changed_by": "[email protected]",
"changed_at": "2026-03-29T12:00:00Z",
"change_type": "create",
"change_summary": "Policy created"
}
]
}

snapshot carries the complete policy resource at that version (abridged above). change_type is one of create, update, enable, disable, delete.

Import and export

POST /api/v1/dynamic-policies/import takes {"policies": [...], "overwrite_mode": "skip" | "overwrite" | "error"} where each entry uses the create-request schema. At least one policy is required, at most 100 per request, and every entry's category must start with dynamic- or media-. The response is 200 with:

{
"created": 4,
"updated": 1,
"skipped": 0,
"errors": []
}

GET /api/v1/dynamic-policies/export returns 200 with {"policies": [...], "exported_at": "...", "tenant_id": "..."}, filtered to dynamic-* and media-* policies.

Effective policies

GET /api/v1/dynamic-policies/effective returns the same policies + pagination shape as the list endpoint, restricted to enabled policies (both dynamic-* and media-*), sorted by priority ascending, up to 100 entries.

What To Watch For

  • The public docs use tenant policy terminology, but the path remains /api/v1/dynamic-policies.
  • Category validation is strict. If you use an unsupported category prefix, the handler rejects the request.
  • For production governance programs, tenant policies are where you usually encode business-specific risk rules, escalation rules, cost thresholds, and media-governance conditions that go beyond built-in system policy coverage.

Platform v7.2.0 Changes

Three updates to the policy surface shipped in platform v7.2.0. They are additive; existing tenant-policy callers keep working unchanged.

context_aware is a valid type

Three seeded system policies (Tenant Isolation, Debug Mode Restriction, Sensitive Data Control) ship with type=context_aware. Before v7.2.0, PUT /api/v1/policies/{id} rejected the update payload with VALIDATION_ERROR because the type allowlist omitted that value, so the Portal's Edit-policy flow 400'd for all three policies. The canonical allowlist is content, user, risk, cost, context_aware, media, rate-limit, budget, time-access, role-access, mcp, connector. No SDK changes required; the SDKs do not send type on update.

Legacy snake-case policy IDs accepted by per-policy endpoints

Per-policy endpoints (GET / PUT / DELETE / POST /test / GET /versions) previously rejected seeded policy IDs like sensitive_data_control and tenant_isolation with Invalid policy ID format because the validator only accepted UUIDs and the sys_* prefix. The validator now also accepts the legacy snake-case form. UUID and sys_* forms continue to work.

GET /api/v1/policies honours tier and category

The unified cross-tier listing endpoint at /api/v1/policies (used primarily by the Customer Portal) now honours tier (system, organization, tenant) and category (security-*, pii-*, dynamic-*, etc.) query params at the handler boundary. The repo supported these params before v7.2.0 but the handler dropped them, so every Tier / Category dropdown in the Portal's Policies page returned the full unfiltered list.

curl "http://localhost:8080/api/v1/policies?tier=system&category=security-sqli" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)"

Operational Readiness Checklist

Before relying on this page in a production rollout, pair it with the core operations docs: