Skip to main content

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/v1 handlers
  • recurring error codes such as VALIDATION_ERROR, NOT_FOUND, INTERNAL_ERROR, UNAUTHORIZED, CONFLICT, and TIER_RESTRICTED

That means the safest production integration strategy is:

  1. branch on HTTP status first
  2. inspect error.code when present
  3. log the full body for operator debugging

Common HTTP Statuses

Status CodeMeaningDescription
200OKRequest successful
201CreatedResource created
204No ContentDelete or success without a body
400Bad RequestInvalid request parameters
401UnauthorizedMissing auth or missing tenant scope on some handlers
403ForbiddenTier restriction, proxy restriction, or governance denial
404Not FoundEndpoint or resource not found
409ConflictDuplicate resource or incompatible state
500Internal Server ErrorUnexpected 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 codeWhere you will see it
VALIDATION_ERRORPolicy APIs, provider APIs, and other typed control-plane handlers
INVALID_JSONTemplate and policy handlers when the request body is malformed
INVALID_REQUESTMedia governance and some gateway or legacy handlers
UNAUTHORIZEDMissing tenant scope or auth on structured handlers
TENANT_REQUIREDTenant-scoped handlers called without X-Tenant-ID. Added in platform v7.2.0.
NOT_FOUNDMissing providers, policies, templates, executions, or steps
CONFLICTDuplicate provider names or resource conflicts
IDEMPOTENCY_KEY_MISMATCHWCP /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_ERRORLicense-gated provider creation failures
TIER_RESTRICTEDPaid-tier-only config such as media governance writes
FEATURE_REQUIRES_EVALUATION_LICENSEEvaluation-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_ERRORBackend or storage failures

Practical Guidance

  • Treat 400 and 401 as client-fixable issues.
  • Treat 403 as either a governance restriction, a tier restriction, or a deployment routing issue.
  • Treat 404 as a missing resource or a wrong endpoint.
  • Treat 500 as retriable operational failure after logging enough context.

For example:

  • POST /api/v1/dynamic-policies can return VALIDATION_ERROR
  • POST /api/v1/llm-providers can return CONFLICT or LICENSE_ERROR
  • POST /api/v1/workflows/{id}/steps/{step_id}/gate and /complete can return 409 IDEMPOTENCY_KEY_MISMATCH when the supplied idempotency_key does not match the key recorded on the step's first gate call
  • PUT /api/v1/media-governance/config can return TIER_RESTRICTED
  • replay routes can return NOT_FOUND for 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/export
  • GET /api/v1/evidence/summary
  • POST /api/v1/policies/simulate
  • POST /api/v1/policies/{id}/impact-report
  • POST /api/v1/cost/estimate
  • GET /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}/explain401 with body {"error": "X-Tenant-ID header is required"}
  • POST /api/v1/audit/search401 with body {"error": "tenant scoping required"}
  • GET /api/v1/audit/tenant/{tenant_id}401 with body {"error": "X-Tenant-ID header is required"} plus a 403 tenant scope mismatch when 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)
}