---
title: "Pre-check request before LLM call"
method: POST
path: "/api/policy/pre-check"
tags: ["Gateway Mode"]
---

# Pre-check request before LLM call

`POST /api/policy/pre-check`

## Request

- Base URL: `https://agent.getaxonflow.com`
- URL: `https://agent.getaxonflow.com/api/policy/pre-check`
- Auth: none declared. Required headers: `Authorization`
- TypeScript: <https://skmtc.dev/getaxonflow/apis/axonflow-agent-api/docs/api/policy/pre-check/post.ts> (a function with typed inputs; its response is checked against the schema)

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
```

## Headers

- `Authorization` string, required
- `X-Axonflow-PEP-Handshake` string: base64url (padding optional) of the PEPHandshake document.

## Request body

- PreCheckRequest
  - `query` string, required: Query to validate
  - `user_token` string: JWT token for user authentication
  - `client_id` string, required: Client application ID
  - `data_sources` string[]: MCP connectors to fetch data from
  - `context` object: Additional context

## Response `200`

Pre-check result

- PreCheckResponse
  - `decision_id` string: The decision identifier, and the CANONICAL name for it. Every other plane that mints a decision calls it `decision_id`: `POST /api/v1/decide`, `POST /api/v1/mcp/check-input`, `POST /api/v1/mcp/check-output`, and the AuthZEN adapter's `context.decision_id`. IT IS THE KEY TO ANOTHER ENDPOINT, and that linkage was previously documented nowhere: * `GET /api/v1/decisions/{decision_id}/explain` returns the full policy explanation for this decision. A caller that reads only `context_id` below still has the value, but nothing told it that the value works on that endpoint, so integrations built against this plane silently lost a capability that integrations built against `/api/v1/decide` got for free. `POST /api/v1/overrides` was the second such endpoint until v11.0.0. It is keyed on a policy, never on `decision_id`, and from v11.0.0 it writes nothing and answers `409 LEGACY_POLICY_WRITE_FROZEN` (#4252). Also valid for the subsequent `POST /api/audit/llm-call` for five minutes, which is what `context_id` was originally named for.
  - `verdict` 'allow' | 'deny': 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` boolean: Whether the request is allowed. RETAINED and not deprecated - every shipped SDK reads it. Prefer `verdict`.
  - `context_id` string: 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` object: Data fetched from MCP connectors
  - `policies` string[]: 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` RateLimitInfo
    - `limit` integer: Rate limit per window
    - `remaining` integer: Remaining requests in current window
    - `reset_at` string, date-time: When the rate limit resets
  - `expires_at` string, date-time: When the context expires
  - `block_reason` string: Reason if request was blocked
  - `trace_id` string: 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.
  - `engine` 'anchored': 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` string: 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` string: 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` object[]: 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.
    - `validator` 'indonesia_pii' | 'india_pii', required
    - `action` 'blocked' | 'masked', required

## Other responses

- `400`: Invalid request body or parameters
- `401`: 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).
- `402`: Budget exceeded — a configured cost budget blocks this request (Enterprise cost controls).
- `403`: Access denied by policy or tenant mismatch
- `429`: Community SaaS tenants past the daily request cap (written by the auth middleware; shared rate-limit envelope).
- `503`: Circuit breaker is open — an emergency stop matching this request's scope is active.

---

[API](https://skmtc.dev/getaxonflow/apis/axonflow-agent-api.md) - [All operations](https://skmtc.dev/getaxonflow/apis/axonflow-agent-api/llms.txt) - [OpenAPI document](https://skmtc.dev/getaxonflow/apis/axonflow-agent-api/revisions/34d30fcb7fa1?raw)
