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_` (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(`@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