API Error Codes
AxonFlow does not have one universal error enum shared perfectly across every handler. Different subsystems use slightly different response helpers. What is consistent enough for engineers to build against is:
- standard HTTP status codes
- common JSON error envelopes on most
api/v1handlers - recurring error codes such as
VALIDATION_ERROR,NOT_FOUND,INTERNAL_ERROR,UNAUTHORIZED,CONFLICT, andTIER_RESTRICTED
That means the safest production integration strategy is:
- branch on HTTP status first
- inspect
error.codewhen present - log the full body for operator debugging
Common HTTP Statuses
| Status Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request successful |
| 201 | Created | Resource created |
| 204 | No Content | Delete or success without a body |
| 400 | Bad Request | Invalid request parameters |
| 401 | Unauthorized | Missing auth or missing tenant scope on some handlers |
| 403 | Forbidden | Tier restriction, proxy restriction, or governance denial |
| 404 | Not Found | Endpoint or resource not found |
| 409 | Conflict | Duplicate resource or incompatible state |
| 500 | Internal Server Error | Unexpected server error |
Common Error Envelope
Many orchestrator handlers return:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed"
}
}
Some older handlers instead return plain string bodies such as:
Connector not found
or:
{
"error": "Invalid request body"
}
So production clients should not assume every route returns the exact same envelope.
Frequently Seen Error Codes
| Error code | Where you will see it |
|---|---|
VALIDATION_ERROR | Policy APIs, provider APIs, and other typed control-plane handlers |
INVALID_JSON | Template and policy handlers when the request body is malformed |
INVALID_REQUEST | Media governance and some gateway or legacy handlers |
UNAUTHORIZED | Missing tenant scope or auth on structured handlers |
TENANT_REQUIRED | Tenant-scoped handlers called without X-Tenant-ID. Added in platform v7.2.0. |
NOT_FOUND | Missing providers, policies, templates, executions, or steps |
CONFLICT | Duplicate provider names or resource conflicts |
IDEMPOTENCY_KEY_MISMATCH | WCP /gate or /complete supplied an idempotency_key that does not match the key recorded on the step's first gate call. Returns 409. See Retry Semantics & Idempotency. |
LICENSE_ERROR | License-gated provider creation failures |
TIER_RESTRICTED | Paid-tier-only config such as media governance writes |
FEATURE_REQUIRES_EVALUATION_LICENSE | Evaluation-tier-or-higher feature attempted on a Community license. Examples: evidence export, policy simulation, retry-aware policy conditions (step.* fields on WCP policies). Returns 403. Upgrade path: getaxonflow.com/evaluation-license. |
INTERNAL_ERROR | Backend or storage failures |
Practical Guidance
- Treat
400and401as client-fixable issues. - Treat
403as either a governance restriction, a tier restriction, or a deployment routing issue. - Treat
404as a missing resource or a wrong endpoint. - Treat
500as retriable operational failure after logging enough context.
For example:
POST /api/v1/dynamic-policiescan returnVALIDATION_ERRORPOST /api/v1/llm-providerscan returnCONFLICTorLICENSE_ERRORPOST /api/v1/workflows/{id}/steps/{step_id}/gateand/completecan return409 IDEMPOTENCY_KEY_MISMATCHwhen the suppliedidempotency_keydoes not match the key recorded on the step's first gate callPUT /api/v1/media-governance/configcan returnTIER_RESTRICTED- replay routes can return
NOT_FOUNDfor missing executions or steps
Missing Tenant Scope (401)
Platform v7.2.0 adds a dedicated TENANT_REQUIRED response for tenant-scoped handlers that previously fell back to an empty-string tenant when neither X-Tenant-ID nor a session-stored tenant was set. The fallback silently scoped SQL to WHERE tenant_id = '', so legitimate calls without the header returned zero rows while sharing a global empty-tenant quota bucket with every other unauthenticated caller.
{
"error": {
"code": "TENANT_REQUIRED",
"message": "Missing tenant context: caller must provide X-Tenant-ID header"
}
}
Handlers that now fail closed with the structured 401 TENANT_REQUIRED response above:
POST /api/v1/evidence/exportGET /api/v1/evidence/summaryPOST /api/v1/policies/simulatePOST /api/v1/policies/{id}/impact-reportPOST /api/v1/cost/estimateGET /api/v1/plans/{id}/cost
A few other tenant-scoped handlers also require X-Tenant-ID and reject the call with 401 when it is missing, but return a plain-text error message rather than the structured TENANT_REQUIRED envelope:
GET /api/v1/decisions/{id}/explain—401with body{"error": "X-Tenant-ID header is required"}POST /api/v1/audit/search—401with body{"error": "tenant scoping required"}GET /api/v1/audit/tenant/{tenant_id}—401with body{"error": "X-Tenant-ID header is required"}plus a403 tenant scope mismatchwhen the URL tenant differs from the header
In practice the agent proxy injects X-Tenant-ID from the authenticated Basic-auth client, so well-formed SDK and Portal traffic never sees either error shape. They surface most often when a caller hits the orchestrator directly without the proxy-auth layer.
Policy Denial (403)
The most common error in production. Returned when a governance policy blocks the request:
{
"error": "Request blocked by dynamic policy"
}
Older or subsystem-specific handlers may also include extra metadata, but the safe integration assumption is that policy denials are not normalized to one shared POLICY_DENIED envelope everywhere.
Rate Limiting (429)
Returned when the client exceeds configured rate limits:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded",
"details": {
"limit": 100,
"remaining": 0,
"reset_at": "2026-03-29T12:00:00Z"
}
}
}
MCP Policy Block (403)
Returned by check-input or check-output when MCP policy evaluation blocks:
{
"allowed": false,
"block_reason": "Blocked by policy sys_sqli_union_based",
"policies_evaluated": 3
}
Note: MCP check endpoints return a flat response (not the nested error envelope).
SDK Error Handling
Python
from axonflow.exceptions import (
AxonFlowError,
PolicyViolationError,
RateLimitError,
AuthenticationError,
)
try:
response = client.proxy_llm_call(...)
except PolicyViolationError as e:
print(f"Blocked by policy: {e.block_reason}")
except RateLimitError as e:
print(f"Rate limited, retry after: {e.reset_at}")
except AuthenticationError:
print("Invalid credentials")
except AxonFlowError as e:
print(f"API error {e.status_code}: {e.message}")
TypeScript
import { AxonFlowError } from '@axonflow/sdk';
try {
const response = await axonflow.proxyLLMCall({...});
} catch (error) {
if (error instanceof AxonFlowError) {
if (error.statusCode === 403) {
console.error('Policy blocked:', error.message);
} else if (error.statusCode === 429) {
console.error('Rate limited');
}
}
}
Go
response, err := client.ProxyLLMCall("user-123", "query", "chat", nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if response.Blocked {
fmt.Printf("Blocked by policy: %s\n", response.BlockReason)
}
