Policy API
Compose a policy, sign it, activate it, evaluate proposed actions against it, and lint or simulate before signing. Every Policy route is tenant-scoped: the {tenant_id} (UUID) appears in the path, and your token's tenant must match.
Get the active policy
GET /v1/policy/{tenant_id}
Compose a candidate policy
POST /v1/policy/{tenant_id}/compose
Sign + activate
POST /v1/policy/{tenant_id}/sign
List versions
GET /v1/policy/{tenant_id}/versions
Evaluate an action
POST /v1/policy/{tenant_id}/evaluate
Lint a draft
POST /v1/policy/{tenant_id}/lint
Simulate against a version
POST /v1/policy/{tenant_id}/simulate
Replay a period
POST /v1/policy/{tenant_id}/simulate-historical
Diff two versions
POST /v1/policy/{tenant_id}/diff
List headless limits
GET /v1/policy/headless-limits
Upsert a headless limit
PUT /v1/policy/headless-limits
Delete a headless limit
DELETE /v1/policy/headless-limits
There is no separate register or revoke endpoint. Activation happens at sign, and superseding a version means signing a new one. The previous active version is recorded in versions history.
Headless Limits
Headless limits are server-managed policy rules for decision-only external authorizations. They let a tenant admin set a per-agent, per-action, per-currency approval envelope without EIP-712 signing.
They are the policy surface behind the headless launch path:
An owner creates a tenant agent and receives one
brain_ak_...key.The agent exchanges that key for a five-minute JWT scoped to
payment_intent:propose.The agent calls
POST /v1/authorizationswithexecution: "external".The active policy plus any headless limit rules returns
approved,needs_review, orblocked.
The route set is:
GET requires policy:read. PUT and DELETE require policy:write and an active admin member, either from a user session or a tenant service credential that asserts that member. Agent principals are refused.
PUT request:
Allowed action types are ach_outbound, wire, card, and erp_writeback. Settlement and custody actions are refused.
The API merges one generated rule per agent, action, and currency into the active policy. It never replaces unrelated tenant rules. Generated rules carry external_authorization_only: true and managed_by: headless_limits_api, are unsigned by design, and only apply to POST /v1/authorizations with execution: external. Internal execution keeps the normal fiat human approval floor.
Every mutation activates a new policy version and emits an audit event with the before and after values. The linter still blocks unsafe generated rules. Auto approval still requires a registered counterparty that is not paused, is not an agent counterparty, and has accepted provenance or verification.
Compose a Candidate Policy
The DSL is structured JSON, not prose. The compose route validates it and returns the canonical hash plus the EIP-712 typed-data payload the tenant signers will sign.
The content wrapper and the numeric version are mandatory. An optional top-level quorum_required sets how many distinct authorized signers sign needs (default 1).
Response:
execute is one of auto | confirm | reject. These are the rule-level outcomes that produce the policy decision (allow | confirm | reject).
Sign and Activate
Each required signer signs the signing_payload from compose, then someone (any caller with policy:sign) submits the policy_id and all signatures together:
200 OK with the serialized policy, an activated flag, and any activation warnings:
The signature count reaching quorum_required flips the policy to active (activated: true). Below quorum, the signatures are recorded and the policy stays pending_signatures. A signature that fails to verify, a duplicate signer, or a signer that is not an authorized tenant signer returns 401 with policy_signature_invalid. Signing a policy that is not awaiting signatures returns 409 with policy_quorum_not_met.
Get the Active Policy
Returns the currently active Policy for the tenant (404 if none has been activated). For a specific historical version, use /versions.
List Versions
Evaluate an Action
Evaluate an action against the active policy. This endpoint does not propose or reserve a payment, but every call emits a policy.evaluate audit event.
The endpoint uses the same policy VM as the section 6 gate, but it is not a complete gate simulation. Its request parser accepts only kind, counterparty_id, amount, agent_role, and timestamp. It does not accept or populate agent_id, tenant_category, confidence, evidence_score, risk_level, spend_in_window, tx_count_in_window, behavior_hash, or action_id. Rules using those inputs can therefore evaluate differently here than they do with a fully populated gate action.
Three Possible Decisions
allow
Rule matched with execute: "auto"; action can proceed straight to the §6 gate
confirm
Rule matched with execute: "confirm"; named required_approvers must sign first
reject
Rule matched with execute: "reject", or no rule matched the default-deny vocabulary
Casing is lowercase. allow | confirm | reject. The historical-simulation counters mirror it (would_allow, would_confirm, would_reject).
Unmatched high-risk agent actions
The policy VM remains deny-by-default. A normal unmatched rule evaluation still returns reject, and low-stakes or money-path proposal types keep their existing fail-closed behavior.
There is one compatibility wrapper for reviewable high-risk non-money proposals: PolicyService.evaluateLegacy converts unmatched agent_action fallthroughs for collections, fraud_anomaly, and vendor_risk to confirm with required_approvers: ["signer"]. The same fallback also applies to stored high-risk action types such as flag_transaction, block_payment, flag_vendor_risk, freeze_card, create_dispute_draft, require_approval, and escalate. This keeps suspicious transactions, vendor-risk holds, and collections follow-ups visible for human review instead of silently closing them as policy rejections. payment proposals are deliberately not included in this fallback.
Decision vocabulary across surfaces
allow | confirm | reject is the canonical protocol decision. The rule-level execute field and the SDK use aliases that map 1:1; the PaymentIntent status reflects the same outcome:
Protocol decision (HTTP/MCP)
Rule-level execute
SDK decision.outcome / action.status
Resulting PaymentIntent status
allow
auto
auto
approved or pending_approval*
confirm
confirm
needs_approval
pending_approval
reject
reject
rejected
rejected
Compare against allow | confirm | reject over HTTP/MCP; the auto | needs_approval | rejected triple is an SDK alias, not the protocol vocabulary.
* An allow decision still enters pending_approval when the hard human-approval floor applies. The floor always applies to onchain_transfer and escrow_release, applies to wire while the fiat floor is enabled, and applies to ach_outbound, ach_inbound, card_payment, and x402_settle unless the matched rule carries a covering signed autonomous amount cap.
Action Vocabulary
The evaluate action.kind is one of the following. There is no rail field on the evaluate action.
kind
Domain
outbound_payment
Money leaving (ACH, wire, on-chain, x402, escrow)
inbound_payment
Money arriving
ledger_write
A Ledger-row mutation (e.g. agent normalization)
onchain_tx
A non-payment on-chain transaction
agent_action
A non-money agent action gated by policy
any
Only valid inside a rule's applies_to catch-all
A rule's applies_to accepts the same kind values, including any. (The PaymentIntent layer uses a separate, broader action_type set. ach_outbound, wire, x402_settle, etc.. Those map onto the kind values internally.)
Proposal read APIs expose a separate public proposal type. Stored agent action_type values are preserved as stored_action_type and then mapped to a public proposal type through the proposal read model. For example, flag_transaction maps to fraud_anomaly, block_payment maps to vendor_risk, and propose_match maps to reconciliation. Ambiguous stored actions such as notify, escalate, create_task, and recommend_action use the agent role or kind instead of action-name guessing.
Lint a Draft
Before composing, run a linter against a policy-content blob to catch shape / semantic problems:
Simulate Against a Version
Replay a single action against a specific historical policy version:
Returns { "decision": <same shape as /evaluate>, "policy_version": 3 }. Unlike /evaluate, simulate wraps the decision with the policy_version it replayed against.
Replay a Period
Replay every action in a time window against a candidate (unsigned) policy. Useful for asking "would version 5 have changed anything?":
Diff Two Versions
What's Next
Last updated
