Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Axonflow Gateway Mode API
version: 11.1.0
contact:
name: AxonFlow Support
url: https://getaxonflow.com/support
license:
name: Business Source License 1.1
url: https://github.com/getaxonflow/axonflow/blob/main/LICENSE
description: 'Operations tagged Gateway Mode across 2 of this provider''s published API definitions: axonflow-agent-api.yaml, axonflow-agent-openapi.yml. Each path carries the servers of the definition it was published in.'
servers:
- url: https://agent.getaxonflow.com
description: Production (SaaS)
- url: https://axonflow.example.com
description: Self-hosted deployment (agent single entry point, ADR-024)
- url: http://localhost:8080
description: Local Development
tags:
- name: Gateway Mode
description: Pre-check and audit for SDK-managed LLM calls
paths:
/api/policy/pre-check:
post:
tags:
- Gateway Mode
summary: Pre-check request before LLM call
description: 'Gateway Mode Step 1: Call this endpoint before making your own LLM API call.
The Agent validates the request against policies and returns:
- `verdict` - the canonical `allow` | `deny`, the same vocabulary
`POST /api/v1/decide` uses. Read this one. Since v11 the pre-check
never holds a request: a `require_approval` policy is a deny.
- `approved: true` if the request is allowed (retained)
- `decision_id` — the decision identifier. Use it for the subsequent
audit call, AND for `GET /api/v1/decisions/{decision_id}/explain`,
which is keyed on it.
- `context_id` — a deprecated alias of `decision_id`, same value
- Optional `approved_data` from MCP connectors
- Rate limit information
**`approved_data` is only prefetched for clean approvals** (#2868):
when the request is blocked, requires HITL approval, or requires
redaction, connector prefetch is skipped and `approved_data` is
never populated — governed data is not fetched for a request that
may not proceed.
**Context expires after 5 minutes.**
## Example Flow
```
1. SDK calls pre-check → gets context_id, approved=true
2. SDK makes direct LLM call (OpenAI, Anthropic, etc.)
3. SDK calls audit with context_id and response metadata
```'
operationId: gatewayPreCheck
parameters:
- $ref: '#/components/parameters/LicenseKey'
- $ref: '#/components/parameters/AxonflowPEPHandshake'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PreCheckRequest'
examples:
basic:
summary: Basic pre-check
value:
query: What is the customer's order status?
user_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
client_id: customer-portal
withDataSources:
summary: Pre-check with data sources
value:
query: Find flights from NYC to LAX next week
user_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
client_id: travel-app
data_sources:
- amadeus
context:
departure_date: '2025-01-20'
return_date: '2025-01-25'
responses:
'200':
description: Pre-check result
content:
application/json:
schema:
$ref: '#/components/schemas/PreCheckResponse'
examples:
approved:
summary: Request approved
value:
context_id: ctx_abc123def456
approved: true
policies:
- pii-detection
- rate-limit
rate_limit:
limit: 1000
remaining: 995
reset_at: '2025-01-15T11:00:00Z'
expires_at: '2025-01-15T10:35:00Z'
piiRedaction:
summary: PII detected - flagged for redaction
description: Returned when the matched PII policy's resolved request-phase action is redact - its stored action, or an organization's pii=redact detection-posture override (since v11 no environment variable sets it). Request approved but PII will be redacted in response.
value:
context_id: ctx_abc123def456
approved: true
requires_redaction: true
policies:
- pii-ssn
expires_at: '2025-01-15T10:35:00Z'
blocked:
summary: Request blocked (resolved action block - a stored block action or an organization's pii=block override)
value:
context_id: ctx_abc123def456
approved: false
policies:
- pii-credit-card
block_reason: Query contains credit card number
expires_at: '2025-01-15T10:35:00Z'
withData:
summary: Approved with data
value:
context_id: ctx_abc123def456
approved: true
approved_data:
amadeus:
rows:
- flight_number: UA123
departure: '2025-01-20T08:00:00Z'
price: 299.99
row_count: 5
duration_ms: 450
policies:
- pii-detection
expires_at: '2025-01-15T10:35:00Z'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
description: 'Budget exceeded — a configured cost budget blocks this request
(Enterprise cost controls).
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
$ref: '#/components/responses/Forbidden'
'429':
description: 'Community SaaS tenants past the daily request cap (written by
the auth middleware; shared rate-limit envelope).
'
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitEnvelope'
'503':
description: 'Circuit breaker is open — an emergency stop matching this
request''s scope is active.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
servers:
- url: https://agent.getaxonflow.com
description: Production (SaaS)
- url: https://axonflow.example.com
description: Self-hosted deployment (agent single entry point, ADR-024)
- url: http://localhost:8080
description: Local Development
/api/audit/llm-call:
post:
tags:
- Gateway Mode
summary: Audit LLM call after completion
description: 'Gateway Mode Step 2: Call this endpoint after your LLM API call completes.
Records:
- Token usage for billing and quotas
- Latency metrics
- Provider and model information
- Estimated cost
**Requires a valid context_id from pre-check (not expired).**'
operationId: auditLLMCall
parameters:
- $ref: '#/components/parameters/LicenseKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AuditLLMCallRequest'
example:
context_id: ctx_abc123def456
client_id: travel-app
response_summary: Found 5 flights matching criteria
provider: openai
model: gpt-4
token_usage:
prompt_tokens: 150
completion_tokens: 200
total_tokens: 350
latency_ms: 1250
metadata:
request_type: travel_search
cache_hit: false
responses:
'200':
description: Audit recorded
content:
application/json:
schema:
$ref: '#/components/schemas/AuditLLMCallResponse'
example:
success: true
audit_id: aud_xyz789
'400':
description: Invalid or expired context
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error: Invalid or expired context
'401':
$ref: '#/components/responses/Unauthorized'
servers:
- url: https://agent.getaxonflow.com
description: Production (SaaS)
- url: https://axonflow.example.com
description: Self-hosted deployment (agent single entry point, ADR-024)
- url: http://localhost:8080
description: Local Development
components:
schemas:
AuditLLMCallRequest:
type: object
required:
- context_id
- client_id
- provider
- model
- token_usage
properties:
context_id:
type: string
description: Context ID from pre-check
client_id:
type: string
description: Client application ID
response_summary:
type: string
description: Brief summary of LLM response (for audit)
maxLength: 500
provider:
type: string
description: LLM provider name
enum:
- openai
- azure-openai
- anthropic
- bedrock
- ollama
- gemini
model:
type: string
description: Model identifier
example: gpt-4
token_usage:
$ref: '#/components/schemas/TokenUsage'
latency_ms:
type: integer
description: LLM call latency in milliseconds
metadata:
type: object
additionalProperties: true
description: Additional metadata for audit
RateLimitEnvelope:
type: object
description: 'Shared tier rate-limit envelope written by the Community SaaS
limiter for daily-quota 429s (the same shape is used with 403 for
Pro-only feature limits). On the REST routes, per-minute 429s use
a plain `{"error": "..."}` body with only a Retry-After header —
not this envelope. On `/api/v1/mcp-server` both limits use it
(`per_minute` and `daily_quota`, 429, #4261), and so do the
`tools/call` tier gates (403, #4274) and the tier admission
refusals on every method (403, or 429 while the admission ledger
cannot be reached; #4249 row 5682255301), wrapped in a JSON-RPC
result.
Accompanied by the `X-Axonflow-Tier-Limit` and
`X-Axonflow-Upgrade-URL` headers, and by `Retry-After` when the
limit has a reset time (not for `feature_pro_only`). Source of truth:
`platform/agent/community_saas_ratelimit_response.go`
(rateLimitEnvelope).
'
properties:
error:
type: string
limit_type:
type: string
description: 'Which limiter fired: `daily_quota`, `per_minute` (MCP server
only), `hitl_approvals_window`,
`feature_pro_only`, or the refused admission dimension
(`service_principal` / `human_principal`, MCP server only).
'
tier:
type: string
limit:
type: integer
remaining:
type: integer
window:
type: string
resets_at:
type: string
format: date-time
upgrade:
type: object
properties:
tier:
type: string
wording:
type: string
compare_url:
type: string
buy_url:
type: string
description: 'Empty on a tier admission refusal: the V1 buy link is
Plugin Pro''s, which lifts no edition ceiling.
'
code:
type: string
description: 'The refusal''s machine-readable code where it has one: a tier
admission refusal''s `ERR_TIER_LIMIT_<DIMENSION>` (MCP server).
Omitted on every other limit.
'
AuditLLMCallResponse:
type: object
properties:
success:
type: boolean
audit_id:
type: string
description: Unique audit record ID
RateLimitInfo:
type: object
properties:
limit:
type: integer
description: Rate limit per window
remaining:
type: integer
description: Remaining requests in current window
reset_at:
type: string
format: date-time
description: When the rate limit resets
TokenUsage:
type: object
properties:
prompt_tokens:
type: integer
description: Tokens in the prompt
completion_tokens:
type: integer
description: Tokens in the completion
total_tokens:
type: integer
description: Total tokens used
PreCheckRequest:
type: object
required:
- query
- client_id
properties:
query:
type: string
description: Query to validate
minLength: 1
user_token:
type: string
description: JWT token for user authentication
client_id:
type: string
description: Client application ID
data_sources:
type: array
items:
type: string
description: MCP connectors to fetch data from
context:
type: object
additionalProperties: true
description: Additional context
PreCheckResponse:
type: object
properties:
decision_id:
type: string
description: "The decision identifier, and the CANONICAL name for it. Every other\nplane that mints a decision calls it `decision_id`:\n`POST /api/v1/decide`, `POST /api/v1/mcp/check-input`,\n`POST /api/v1/mcp/check-output`, and the AuthZEN adapter's\n`context.decision_id`.\n\nIT IS THE KEY TO ANOTHER ENDPOINT, and that linkage was\npreviously documented nowhere:\n\n * `GET /api/v1/decisions/{decision_id}/explain` returns the full\n policy explanation for this decision.\n\nA caller that reads only `context_id` below still has the value, but\nnothing told it that the value works on that endpoint, so\nintegrations built against this plane silently lost a capability\nthat integrations built against `/api/v1/decide` got for free.\n\n`POST /api/v1/overrides` was the second such endpoint until\nv11.0.0. It is keyed on a policy, never on `decision_id`, and from\nv11.0.0 it writes nothing and answers\n`409 LEGACY_POLICY_WRITE_FROZEN` (#4252).\n\nAlso valid for the subsequent `POST /api/audit/llm-call` for five\nminutes, which is what `context_id` was originally named for.\n"
verdict:
type: string
enum:
- allow
- deny
description: 'The CANONICAL answer to "may I do this?", in the same vocabulary and
with the same values `POST /api/v1/decide` returns.
Read this rather than `approved` in new integrations. Across the
governed surface the same question was answered by five different
keys in two different types - `verdict` (string) on
`/api/v1/decide`, `approved` (bool) here, `allowed` (bool) on both
MCP check endpoints, `decision` (bool) on the AuthZEN adapter and
`decision` (string) on the decisions feed - so a client typed from
one plane could not deserialise another.
Since v11 the pre-check never holds a request: a `require_approval`
policy is a deny, because an anchored CHALLENGE is a refusal. So
`verdict` and `approved` never disagree.
The AuthZEN adapter (`POST /api/v1/access/evaluation`) keeps its
boolean `decision` and is not a divergence to be fixed: AuthZEN 1.0
mandates a boolean, and the four-valued state rides in that
endpoint''s response context behind profile negotiation.
'
approved:
type: boolean
description: 'Whether the request is allowed. RETAINED and not deprecated - every
shipped SDK reads it. Prefer `verdict`.
'
context_id:
type: string
deprecated: true
description: 'DEPRECATED ALIAS of `decision_id`, carrying the identical value.
Retained because every shipped SDK reads it and removing it would
break them all. New integrations should read `decision_id`; this
member will be removed no earlier than the release after the one
that introduced `decision_id`.
'
approved_data:
type: object
additionalProperties: true
description: Data fetched from MCP connectors
policies:
type: array
items:
type: string
description: 'Policies that were evaluated.
`segment_resolution_failed` no longer appears here (it did from
#3312). No segment gate stands on the pre-check any more: it
resolved the caller''s governance segments and refused the request
when that failed, on behalf of an organization''s segment-scoped
static rows. Those rows no longer decide: the anchored engine
authors this verdict and reads no segments (PRD v11 §1.1, §1.2).
'
rate_limit:
$ref: '#/components/schemas/RateLimitInfo'
expires_at:
type: string
format: date-time
description: When the context expires
block_reason:
type: string
description: Reason if request was blocked
trace_id:
type: string
description: 'W3C OpenTelemetry trace_id (32-char lowercase hex) emitted
by the decision tracer. Optional: present when the tracer
is enabled via AXONFLOW_OTEL_ENDPOINT, omitted otherwise.
Policy Enforcement Points propagate this id downstream so
multi-gateway decisions stitch into one end-to-end trace.
'
example: b3a1f1f3a8c6e0d791bc3e7a8c2d5f4a
engine:
type: string
enum:
- anchored
description: 'Which policy engine authored this verdict: the ADR-065 decision
plane, the only author on this route (PRD v11 §1.1). Omitted
on a refusal no engine decided - an authentication failure, or a
request refused before the policy pass ran.
'
subject_type:
type: string
description: 'The type of principal the verdict was decided for (PRD v11 §1.6):
`User` for a verified user token, `Client` when the request
presented no user identity and its client credential is the
principal. Omitted wherever `engine` is.
'
policy_bundle:
type: string
description: 'The digest of the policy set that decided: the system corpus''s
restriction for this route and the organization root - the
organization''s active typed document composed with the
deployment''s baseline permission pack, or, while it has published
nothing, the implicit bundle of that pack and the organization
template. A rollback reinstates an earlier digest. Omitted wherever
`engine` is.
'
legacy_validators:
type: array
description: 'A checksum validator that acted BEFORE the anchored engine decided
(#4122): under an organization''s recorded `pii=block` or
`pii=redact` detection override, the Indonesia or India validator
blocked the request or masked the response ahead of the decision
plane. Omitted when none did, which is every request without
such an override.
'
items:
type: object
required:
- validator
- action
properties:
validator:
type: string
enum:
- indonesia_pii
- india_pii
action:
type: string
enum:
- blocked
- masked
ErrorResponse:
type: object
description: 'Handler-written error envelope. Note the agent has a second error
envelope for middleware-written errors (see JSONError) — clients
should tolerate both shapes on 4xx/5xx.
'
properties:
success:
type: boolean
example: false
error:
type: string
description: Error message
responses:
Unauthorized:
description: 'Missing or invalid authentication. Handler-written 401s use the
`{success, error}` envelope; 401s written by the auth middleware
use the `{"error": {"code", "message"}}` envelope (JSONError).
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error: 'Authentication required: provide Authorization header with Basic auth (clientId:clientSecret)'
Forbidden:
description: Access denied by policy or tenant mismatch
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error: Tenant mismatch
BadRequest:
description: Invalid request body or parameters
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error: Invalid request body
parameters:
AxonflowPEPHandshake:
name: X-Axonflow-PEP-Handshake
in: header
required: false
description: 'The ADR-065 **PEP capability handshake**: base64url of a compact JSON
document in which an external enforcement point declares what it is and
which obligations it can discharge. See `PEPHandshake` for the document.
**Absent is the default and changes nothing.** A caller that omits the
header takes byte-for-byte the path it took before this header existed.
**What an absent header means for a redaction depends on the plane, by
design (PRD v11 section 1 item 16, #4257).** The MCP passes discharge a
redaction of the content they hand back: the request pass
(`check-input`, `check_policy`) masks the statement, and a redaction
that masks nothing in the statement is refused `unsupported_obligation`
to every caller, and one that masks a request parameter to every caller
that has not declared `field_redact` at version 2 (on Community, to
every caller) (#4264);
the response passes (`check-output`, the MCP server''s `check_output`)
mask the rows or the message. `/api/v1/decide` and the gateway
pre-check return a decision rather than content, so a required
redaction is a `field_redact` obligation for the enforcement point, and
a caller that has not declared `field_redact` is refused
`unsupported_obligation`. A caller that declares NO redaction
(`capabilities: []`) is refused on each of these planes; on
Community the MCP passes still return a checksum validator''s masked
content to it (reachable only through a directly inserted
`detection_action_overrides` row) until #4122.
A header that is PRESENT and cannot be read is **refused**, never
treated as absent: degrading a malformed declaration to "legacy caller"
would go on handing an enforcement point obligations it had just said it
cannot discharge. The refusal is `400` and its message names this header,
which matters on `/api/v1/access/evaluation` where the refusal is
rendered through that surface''s existing `incomplete_evaluation` code and
the message is the only thing distinguishing a malformed HEADER from a
malformed body ENVELOPE.
Present more than once is refused: RFC 7230 permits an intermediary to
join repeated field lines with a comma, and a comma is outside the
base64 alphabet, so a joined pair can only decode to malformed. That is
why the document is base64 rather than raw JSON, which would join into
something a lenient parser might accept.
When a decision carries a **mandatory** obligation the declared set does
not cover, the request is answered `200` with `verdict: deny` and the
reason `unsupported_obligation` (ADR-065 invariant 8) - a decision about
the request, not a transport error. That holds on every edition. An
Enterprise deployment adds a second reason beginning
`pep_capability_unsupported` naming the gap, and refuses a checksum
validator''s mask on the MCP passes; a Community deployment hands that
masked content over (#4257 split 2, #4122).
'
schema:
type: string
maxLength: 4096
description: base64url (padding optional) of the PEPHandshake document.
LicenseKey:
name: Authorization
in: header
required: true
description: 'OAuth2-style Basic authentication header.
Format: `Basic base64(clientId:clientSecret)`
- `clientId`: Your organization identifier (required)
- `clientSecret`: Authentication credential (optional for community mode)
Not required when `DEPLOYMENT_MODE=community`.
'
schema:
type: string
example: Basic bXktb3JnOkFYT04tVjIteHh4
securitySchemes:
BasicAuth:
type: http
scheme: basic
description: "OAuth2-style Basic authentication using `clientId:clientSecret` credentials.\n\n**Header format:** `Authorization: Basic base64(clientId:clientSecret)`\n\n- `clientId` (required): Your organization/client identifier\n- `clientSecret` (optional): Authentication credential. Optional for community/self-hosted mode.\n\n**Example:**\n```bash\n# With clientSecret (enterprise)\ncurl -H \"Authorization: Basic $(echo -n 'my-org:AXON-V2-xxx' | base64)\" ...\n\n# Without clientSecret (community mode)\ncurl -H \"Authorization: Basic $(echo -n 'my-org:' | base64)\" ...\n```\n\n## Per-user identity behind a shared credential\n\nThis credential authenticates an ORGANIZATION or client, not a person.\nBehind one such credential can sit many human principals, each\noptionally forwarding a **per-user token** that proves who they are.\nWhere that token is read depends on the envelope: the `user_token`\nfield of the request body on `POST /api/v1/decide` and the four MCP\nREST routes, and the `X-User-Token` header on the MCP-server JSON-RPC\nplane. The two spellings are deliberately not interchangeable.\n\n**A presented per-user token that fails to validate is a refused\naccess attempt, not a legacy caller** (`401`, audited\n`user_token_rejected`). It is never downgraded to a shared service\nidentity, so revocation, expiry, algorithm pinning and signature\nchecks take effect on every plane that reads one.\n\n**Whether presenting a token is REQUIRED is a per-organization\nposture, `require_user_token`, and it is off by default (#3476).**\nWith it off, an enterprise caller that presents no token at all is\nserved under a synthetic org-scoped service identity\n(`<client-id>@axonflow.local`, role `service`), which is the correct\nanswer for an infrastructure gateway acting as a Policy Enforcement\nPoint with no end-user token to forward. With it on, that caller is\nrefused at AUTHENTICATION, before any policy is evaluated (`401`,\naudited `user_token_required`).\n\nThe posture exists because a policy that names a PERSON - a\nprincipal-scoped constraint or permission in the organization's typed\ndocument (PRD v11 §1.6) - is only meaningful if a caller cannot CHOOSE\nto arrive without an identity: with the posture off such a policy\nstill applies to everyone who presents a token, but a caller can\ndecline to present one and be decided as the credential\n(`subject_type=Client`). Governance segments (ADR-060) decide on no\nagent route since v11.0.0 (#4253). Two levers set it, and an explicit\nper-organization row wins over the deployment-wide default in EITHER\ndirection:\n\n- `organizations.require_user_token`, per organization, default\n `false`.\n- `AXONFLOW_REQUIRE_USER_TOKEN`, deployment-wide, default `false`.\n\nA posture change takes up to one cache window to become live\n(`AXONFLOW_REQUIRE_USER_TOKEN_TTL_SECONDS`, default 60 seconds,\nclamped to `[5, 600]`). A posture that cannot be READ resolves to\nREQUIRED rather than not-required, so a database outage cannot\nquietly switch the control off; a genuinely absent organization row\nis not a read failure and falls through to the deployment default.\n\n`POST /v1/chat/completions` is outside this guarantee: it mirrors\nOpenAI's wire shape and carries no per-user token field at all, so it\nkeeps the synthetic-identity fallback regardless of the posture.\nCommunity and community-SaaS deployments never reach any of the above.\n"
InternalServiceID:
type: apiKey
in: header
name: X-Internal-Service-ID
description: 'Internal-service (operator lane) credential — **part one of two**.
Must be sent together with `X-Internal-Service-Token`; either header
alone is not a credential.
This is the HMAC identity the Orchestrator and the Enterprise
customer-portal use to call agent endpoints without holding a
customer license. `apiAuthMiddleware` lifts both headers (plus an
optional `X-Tenant-ID` scope) into `AuthHints`
(`internalServiceHints` in `platform/agent/auth.go`) and
`Authenticate()` validates them before any mode-specific auth
(`platform/agent/authenticator.go:120-155`).
Value: the service id, `orchestrator-internal`.
⚠️ An invalid or expired token is **not** an error by itself — it
falls through to the deployment''s normal auth
(`platform/agent/authenticator.go:153-154`). Send the internal-service
headers on their own: paired with an `Authorization: Basic` header, a
stale token silently yields a *tenant*-scoped answer that looks like a
successful operator call.
'
InternalServiceToken:
type: apiKey
in: header
name: X-Internal-Service-Token
description: 'Internal-service (operator lane) credential — **part two of two**.
Must be sent together with `X-Internal-Service-ID`.
Format: `AXON-INTERNAL-{unix_ts}-{sig}`, where `sig` is the first 16
hex characters of HMAC-SHA256 over `orchestrator-internal:{unix_ts}`
keyed with `AXONFLOW_INTERNAL_SERVICE_SECRET`. Validated by
`platform/shared/serviceauth` within a 5-minute clock-skew window, so
it must be re-minted per session. See
`technical-docs/runbooks/RUNBOOK_CONNECTOR_CONFIGURATION.md` for the
exact minting snippet.
'
x-refined-from:
- axonflow-agent-api.yaml
- axonflow-agent-openapi.yml