Polivor

Authorize

POST /v1/authorize asks for authority for one action. It needs an agent key and an Idempotency-Key header of 1 to 128 characters.

The request

FieldMeaning
agent_versionThe version of the agent asking, for example 3.7.
mandate_idThe mandate the agent acts under.
actionThe payment: type (PayInvoice), amount_minor, currency, debtor_account, payment_type (SEPA_CT or SEPA_INST), beneficiary (id and iban; a country is accepted but never used) and reference.
signalsOptional hints such as origin and risk_flags. They can make a policy stricter, never more permissive.

Unknown fields are ignored. Flags like iban_changed or beneficiary_allowlisted have no effect, because Polivor computes those facts itself.

The answer

OutcomeHTTPWhat it means
ALLOW200Go ahead. The response carries an authorization.
DENY200Do not pay. reasons says why. No authorization.
REQUIRE_APPROVAL202A person must decide first. No authorization yet; see Approvals and webhooks.

Each decision lists the checks that ran with a result, and the reasons that led to the outcome (a policy id such as agent.may_pay_invoices, or a code such as BUDGET_EXCEEDED, MANDATE_REVOKED, AGENT_VERSION_NOT_AUTHORIZED). The evidence reference points to the signed record of this decision.

Reason codes

A reason is either the id of the policy rule that decided (such as invoice.over_threshold), or one of these codes. Match on the code, not on any wording.

CodeMeaning
AGENT_SUSPENDEDThe agent is suspended.
AGENT_VERSION_NOT_AUTHORIZEDThis agent version is not registered, or the mandate does not allow it.
MANDATE_NOT_FOUNDNo such mandate for this agent. A mandate that belongs to another agent looks the same.
MANDATE_REVOKEDThe mandate is revoked, and revocation is final.
MANDATE_SUSPENDEDThe mandate is suspended.
MANDATE_NOT_ACTIVEThe mandate is not in force, for example a draft.
MANDATE_NOT_VALIDThe time of the request is outside the mandate's validity window.
NO_ACTIVE_POLICYNo policy is published for the tenant.
NO_PERMITNo rule of the active policy permits this payment: it is outside the mandate's scope.
POLICY_EVALUATION_ERRORThe policy could not be evaluated. The payment is denied, never allowed.
BUDGET_EXCEEDEDThe day or month budget is used up.
APPROVER_DENIEDA person denied the payment.
APPROVAL_EXPIREDNobody decided before the approval expired.

A denial looks like this:

{
  "decision_id": "9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
  "outcome": "DENY",
  "reasons": [{ "code": "AGENT_VERSION_NOT_AUTHORIZED" }],
  "checks": [
    { "id": "agent.active", "result": "pass" },
    { "id": "agent.version_registered", "result": "pass" },
    { "id": "agent.version_authorized", "result": "fail" }
  ],
  "mandate": { "id": "2d1b3b44-5e0c-4a3c-9a7e-6f1f7a9c0a11", "version": 1 },
  "policy_version": 1,
  "evidence": { "id": "b53c9b3e-9a1e-4a43-9c5b-2a6f5c6e7d10", "seq": 18 }
}

Idempotency

Send the same Idempotency-Key and the same body and you get the same response back, for 24 hours; nothing is decided twice and the budget is reserved once. The same key with a different body returns 409 IDEMPOTENCY_KEY_REUSED; a key older than 24 hours returns 409 IDEMPOTENCY_KEY_EXPIRED. Use a new key for each new payment.

Money

Amounts are integers in minor units with an ISO 4217 currency. There are no decimals anywhere in the API.

Simulate

POST /v1/simulate takes the same body (plus an optional policy_version to preview a draft policy) and returns the same shape with "simulated": true. It writes nothing, reserves nothing and issues no authorization. It needs an admin key.

Get a decision later

GET /v1/decisions/{id} returns the status: ALLOWED, DENIED or PENDING_APPROVAL, and the authorization once it exists. An agent can read its own decisions.