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.
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:
| Feature | System Policies | Tenant Policies |
|---|---|---|
| API Endpoint | /api/v1/static-policies | /api/v1/dynamic-policies |
| Structure | Pattern-based rules | Conditions and actions |
| Location | Agent | Orchestrator |
| Use Case | PII, SQLi, redaction, fixed governance checks | Business logic, risk, cost, user and workflow controls |
| Scope | Request content | User, 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 derivedX-Tenant-ID/X-Org-ID/X-Client-IDheaders before proxying, overwriting any client-supplied values. - Direct Orchestrator access (port
8081): the handler reads tenant scope from theX-Tenant-IDheader, falling back toX-Org-IDfor backward compatibility. Requests with neither return401with codeUNAUTHORIZED. X-User-IDrecommended on mutating requests for audit attribution (recorded ascreated_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
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/dynamic-policies | List tenant policies |
POST | /api/v1/dynamic-policies | Create a tenant policy |
POST | /api/v1/dynamic-policies/import | Import policies |
GET | /api/v1/dynamic-policies/export | Export policies |
GET | /api/v1/dynamic-policies/effective | Get 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}/test | Test a policy |
GET | /api/v1/dynamic-policies/{id}/versions | List policy versions |
Listing and Filtering
Verified query parameters handled by the list endpoint:
| Query param | Notes |
|---|---|
type | Free-form type filter used by the service |
category | Must start with dynamic- or media- |
search | Search term |
sort_by | Sort field |
sort_dir | Sort direction |
page | Page number, default 1 |
limit | Preferred page size, max 100 |
page_size | Deprecated alias for backward compatibility |
enabled | true 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=mediawithout an explicitcategoryautomatically filters tomedia-*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:
| Field | Notes |
|---|---|
id | Policy ID |
name | Display name |
description | Optional description |
type | content, user, risk, cost, or other supported policy types |
category | Must use a valid dynamic-* or media-* prefix |
tier | system, organization, or tenant |
conditions | Condition list |
actions | Action list |
priority | Evaluation priority |
enabled | Whether the policy is active |
version | Current version |
tenant_id | Tenant scope |
organization_id | Present for organization-tier policies |
tags | Optional tags |
created_at, updated_at | Timestamps |
created_by, updated_by | Audit attribution |
deleted_at | Present 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 charactersdescription- optional, at most 500 characterstype- required, one ofcontent,user,risk,cost,context_aware,media,rate-limit,budget,time-access,role-access,mcp,connectorconditions- at least one; each needs a non-emptyfieldand anoperatorfromequals,not_equals,contains,not_contains,contains_any,regex,greater_than,less_than,in,not_in(aregexoperator's value must be a string and must compile)actions- at least one; eachtypemust be one ofalert,block,log,modify_risk,redact,require_approval,route,warnpriority- 0 to 1000
Tier and licensing rules (violations return 403 with a tier-specific error code):
tierdefaults totenant;systemcannot be created via the APItier: organizationrequires 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
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_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:
| Method | Path | Why it matters |
|---|---|---|
POST | /api/v1/dynamic-policies/{id}/test | Validate a rule before enabling it |
GET | /api/v1/dynamic-policies/export | Export tenant policies for review or migration |
POST | /api/v1/dynamic-policies/import | Import policy sets with overwrite control |
GET | /api/v1/dynamic-policies/{id}/versions | Inspect policy history |
GET | /api/v1/dynamic-policies/effective | See 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:
| Field | Notes |
|---|---|
query | Required - the simulated request text (400 VALIDATION_ERROR when empty) |
user | Optional object of user attributes (email, role, department, ...) |
request_type | Optional request type string |
context | Optional 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_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)"
Related Docs
Operational Readiness Checklist
Before relying on this page in a production rollout, pair it with the core operations docs:
- Deployment Mode Matrix for self-hosted, Evaluation, Enterprise, SaaS, and In-VPC fit
- Failure Modes And Recovery for degraded-provider, connector, approval, and runtime behavior
- Capacity Planning for sizing and growth signals
- Community vs Evaluation vs Enterprise for limits, support surfaces, and upgrade triggers
