Choose mandate constraints
Choose and combine amount, payee, time, category-budget and charge-count rules without widening authority.
In short
1 callFor people and agentsThe same list heads this page's Markdown:/mandate/mandate-constraints.md
payment_amount sets the outer amount or budget. Constraints narrow that authority: which merchants may be paid, how small or large a charge may be, when it may happen, how much one category may consume, and how many charges may settle.
Every supplied constraint must pass. A rule the service does not understand fails closed rather than widening the user's permission.
Start with the intended charge
Verify and exercise evaluate constraints against a charge:
| Field | Required | Meaning |
|---|---|---|
amount | Yes | Integer minor units. |
currency | Yes | Compared exactly with the mandate and relevant constraints. |
payee_id | Yes | Merchant receiving the payment. |
category | For category-scoped rules | Transaction category from GET /v1/mandate/categories. |
at | No | ISO-8601 charge time; defaults to current UTC time. |
Always verify with the exact charge settlement intends to exercise. If charge is omitted, the service checks the token and lifecycle but skips constraints and the top-level currency/amount comparison.
Pick the smallest useful set
| Goal | Constraint | Typical use |
|---|---|---|
| Keep each charge inside a range | payment.amount_range | A fare must be between the quoted minimum and maximum |
| Restrict merchants | payment.allowed_payees | An open travel budget may reach only approved airline and hotel IDs |
| Restrict time | payment.execution_date | Payment may settle only during a booking window |
| Cap one category over time | payment.category_budget | Flights may consume at most USD 1,800 of a larger trip budget |
| Cap the number of settlements | payment.max_charges | A delegated budget may settle no more than three times |
Avoid duplicating a top-level rule unless the narrower constraint communicates real user intent. For example, a closed mandate already names one payee, while an open mandate usually needs payment.allowed_payees.
Amount range
{
"type": "payment.amount_range",
"currency": "USD",
"min": 100000,
"max": 180000
}min and max are inclusive. The charge currency must equal the constraint currency.
Missing min behaves as 0.
Missing max also behaves as 0, so the rule rejects every positive charge. Always set currency and max explicitly.
Allowed payees
{
"type": "payment.allowed_payees",
"allowed": [
{ "id": "airline_demo" },
{ "id": "hotel_demo" }
]
}The charge's payee_id must match one allowed[].id. A missing or empty list rejects every payee.
This is the primary merchant boundary for an open mandate whose top-level payee is omitted.
Execution window
{
"type": "payment.execution_date",
"not_before": "2026-08-23T00:00:00+00:00",
"not_after": "2026-08-30T23:59:59+00:00"
}The charge's at value must fall inside the inclusive window. Each bound is optional. If at is omitted, evaluation uses the current UTC time.
Use timezone-aware values with explicit offsets for the charge and both bounds.
Category budget
{
"type": "payment.category_budget",
"category": "flight",
"currency": "USD",
"max": 180000
}This is a running sub-budget. For a matching charge, the service adds the current amount to prior charged events in that category and requires the total to stay at or below max.
Evaluation behaves as follows:
- no charge category → fail closed,
- different category → this constraint passes as not applicable,
- matching category with different currency → fail,
- matching category inside the remaining sub-budget → pass.
The total open-mandate budget still applies to every category. A category without its own sub-budget is bounded by payment_amount and the other constraints.
Issue validates category names against GET /v1/mandate/categories. A payment.category_budget without category, or with an unknown category, returns HTTP 422.
Category budgets are most useful on open mandates. On a closed mandate there is no prior spend, so the rule acts only as a per-charge cap.
Maximum charges
Mandate-wide:
{
"type": "payment.max_charges",
"max": 3
}Category-specific:
{
"type": "payment.max_charges",
"category": "flight",
"max": 1
}Without category, every successful open-mandate charge counts. With category, only matching charged events count; an unlabeled charge fails closed and a differently labeled charge is outside this particular rule.
On a closed mandate, prior charge count is always zero before its one allowed exercise, so a max of 1 or more adds no useful restriction.
Top-level amount checks still apply
After constraint evaluation, the service compares the charge with payment_amount:
- different currency → failed
currencycheck, - closed amount above the cap → failed
amountcheck, - open amount plus prior spend above the total budget → failed
budgetcheck.
Passing those top-level comparisons does not add a success entry. These checks appear only when they fail.
Unknown and incomplete rules
An unsupported type produces a failed check:
{
"check": "payment.something_else",
"ok": false,
"reason": "unresolved_constraint: payment.something_else"
}Most constraint fields are stored as supplied and evaluated later. Test the full issue → confirm → verify path with representative charges before deploying a new constraint set.
Stateful limits and concurrency
payment.category_budget, payment.max_charges, and the open total budget depend on earlier charges.
Verify reads that ledger without a lock, so its result is advisory. Exercise locks the mandate row, recalculates all stateful limits, and records charged only when they still pass. Concurrent settlement attempts therefore cannot jointly overrun a limit.