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
| Field | Meaning |
|---|---|
agent_version | The version of the agent asking, for example 3.7. |
mandate_id | The mandate the agent acts under. |
action | The 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. |
signals | Optional 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
| Outcome | HTTP | What it means |
|---|---|---|
ALLOW | 200 | Go ahead. The response carries an authorization. |
DENY | 200 | Do not pay. reasons says why. No authorization. |
REQUIRE_APPROVAL | 202 | A 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.
| Code | Meaning |
|---|---|
AGENT_SUSPENDED | The agent is suspended. |
AGENT_VERSION_NOT_AUTHORIZED | This agent version is not registered, or the mandate does not allow it. |
MANDATE_NOT_FOUND | No such mandate for this agent. A mandate that belongs to another agent looks the same. |
MANDATE_REVOKED | The mandate is revoked, and revocation is final. |
MANDATE_SUSPENDED | The mandate is suspended. |
MANDATE_NOT_ACTIVE | The mandate is not in force, for example a draft. |
MANDATE_NOT_VALID | The time of the request is outside the mandate's validity window. |
NO_ACTIVE_POLICY | No policy is published for the tenant. |
NO_PERMIT | No rule of the active policy permits this payment: it is outside the mandate's scope. |
POLICY_EVALUATION_ERROR | The policy could not be evaluated. The payment is denied, never allowed. |
BUDGET_EXCEEDED | The day or month budget is used up. |
APPROVER_DENIED | A person denied the payment. |
APPROVAL_EXPIRED | Nobody 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.