Start with one sentence: “Allow office purchases from this shop, using this card, up to USD 100 per purchase and USD 150 for the complete request.” This page turns that sentence into a policy. The same conditions work for Google AP2 and Mastercard VI.
What is being checked?
Your integration receives the protocol content, builds an intent, asks Verdict for a decision, then signs the evaluated content if authorized. Signing, SD-JWT assembly and cryptographic verification remain in the integration.
A request can contain several actions. You write the rule for one action; Verdict runs it for every action. If one action is denied, the entire request is denied.
| Action kind | What you are authorizing | Fields to check |
|---|---|---|
purchase |
A concrete checkout and its payment together | checkout, payment |
payment |
A concrete payment at a payment-only signing stage | payment |
checkout |
A concrete checkout at a checkout-only signing stage | checkout |
delegation |
Permission to make future purchases within proposed bounds | delegation |
A checkout is the order/cart description. A payment specifies who receives money, the method and the amount. A delegation grants future authority: a maximum on a delegation bounds future spending. Start each allow rule with the permitted action kind.
The data seen by one purchase rule
Relevant fields produced by the mandate decoder:
{
"action": {"kind": "purchase", "index": 0},
"checkout": {
"vct": "mandate.checkout.1",
"content": {"merchant": {"id": "merchant-1", "website": "https://office.example"}}
},
"payment": {
"vct": "mandate.payment.1",
"payee": {"id": "merchant-1", "website": "https://office.example"},
"payment_instrument": {"id": "card-1", "type": "card"},
"payment_amount": {"currency": "USD", "amount": 7500}
},
"request": {"actionCount": 2, "allAmountsKnown": true, "totals": {"USD": 15000}}
}
This action spends USD 75.00. Another action also spends USD 75.00, so the request
total is USD 150.00. payment changes when Verdict evaluates the next action;
request describes the same complete request each time.
Copy this authority
schemaVersion: verdict.authority/v1
id: com.acme.office.ap2
type: ap2.mandate
version: 2.0.0
config:
merchants:
officeShop:
website: https://office.example
methods:
companyCard:
id: card-1
type: card
policy:
id: office-purchases
fallback: DENY
variables:
maxPerPurchase: "100.00"
maxPerRequest: "150.00"
allow:
- id: office-purchase
where:
- "action.kind == 'purchase'"
- "checkout.merchantIs(merchants.officeShop)"
- "payment.payeeIs(merchants.officeShop)"
- "payment.methodIs(methods.companyCard)"
- "payment.amountAtMost(maxPerPurchase, 'USD')"
deny:
- id: request-total
where:
- "!request.totalAtMost(maxPerRequest, 'USD')"
Download AP2 authority or
Mastercard VI authority.
For VI, the authority uses type: mcintent.mandate; the policy expressions are the same.
Read each part in plain language
| Part | Meaning |
|---|---|
config.merchants.officeShop |
A name for an exact merchant identity |
config.methods.companyCard |
A name for one specific card/method |
maxPerPurchase: "100.00" |
A policy constant, in major units (USD dollars here) |
action.kind == 'purchase' |
Only approve concrete checkout/payment pairs |
checkout.merchantIs(merchants.officeShop) |
The shop in the checkout must match this configured shop |
payment.payeeIs(merchants.officeShop) |
The recipient of the money must match this configured shop |
payment.methodIs(methods.companyCard) |
Use this particular configured card |
payment.amountAtMost(maxPerPurchase, 'USD') |
Current payment is in USD and no more than USD 100 |
!request.totalAtMost(maxPerRequest, 'USD') in deny |
Deny if the complete request is not priced entirely in USD within USD 150 |
The shop and payee are separate checks because a checkout's seller can differ from
the payment recipient, for example with an intermediary. Configure both identities
if your payment flow needs that. A merchant entry may use id, website, or both;
all supplied criteria must match. A method needs id and may also specify type.
A display name is never sufficient identity.
The module exposes the authority's identity catalogs as merchants and methods.
Pass a catalog entry to each helper, for example payment.methodIs(methods.companyCard).
Accessing a missing catalog key raises an evaluation error.
Try these cases
All purchases below use the configured shop, payee and method unless stated otherwise.
| Request | Decision | Reason |
|---|---|---|
| One purchase of USD 100 | ALLOW | Meets both limits |
| Two purchases of USD 75 | ALLOW | Each <= 100; together <= 150 |
| Two purchases of USD 80 | DENY | Total 160 |
| One purchase of USD 100.01 | DENY | Individual limit exceeded |
| USD 10 plus EUR 10 | DENY | Request is not entirely in USD |
| One valid purchase plus one using another card | DENY | Every action must pass |
| One valid purchase plus a delegation | DENY | Delegation is not permitted by this rule |
| Payment-only action | DENY | Different signing stage; kind is not purchase |
The total check returns false for unpriced actions or mixed currencies. It sums payments within the current request. Daily or monthly limits require spending history supplied by the application.
Common changes
These snippets replace or add conditions inside the same allow rule. Keep the other checks when adding a new condition.
Permit two shops
Add backupShop to config.merchants, then replace each shop condition as needed:
checkout.merchantIs(merchants.officeShop) || checkout.merchantIs(merchants.backupShop)
payment.payeeIs(merchants.officeShop) || payment.payeeIs(merchants.backupShop)
These conditions allow the seller and payee to match different catalog entries. To require the same shop for both, use:
(checkout.merchantIs(merchants.officeShop) && payment.payeeIs(merchants.officeShop)) ||
(checkout.merchantIs(merchants.backupShop) && payment.payeeIs(merchants.backupShop))
Bound a range or require an exact amount
payment.amountAtLeast('1.00', 'USD')
payment.amountAtMost('100.00', 'USD')
Use both conditions for USD 1–100, inclusive. For an exact charge use
payment.amountEquals('49.99', 'USD'). The protocol amount stays in integer minor
units; the helpers convert the quoted limit. Never write 7500 when you mean a
USD 75.00 helper limit: write '75.00' and 'USD'.
Restrict cart items
For the AP2 UCP checkout shape, add:
size(checkout.content.line_items) > 0
checkout.content.line_items.all(line, line.item.id in ['paper-a4', 'pens-blue'])
checkout.content.line_items.all(line, bigint.lte(line.quantity, '10'))
This permits the listed SKUs with at most ten units per line. Repeated SKUs are
checked separately on each line. Product restrictions come from these conditions;
officeShop identifies the merchant.
VI checkout JSON is integration-defined: use these paths only if your VI adapter
supplies this shape. The common merchant helper expects content.merchant.
Require approval
Add an approvals block to the allow rule and define its named approvers under
policy.approvers, as shown in the approval guide. Keep the amount,
shop and method checks. For a request with several actions, every applicable approval
requirement must be satisfied before the integration signs it.
Approve a payment-only stage
Use action.kind == 'payment', retain the payment identity/method/amount checks, and
omit the checkout condition because no checkout is present. The integration must
select PaymentRequestMode.PAYMENTS as part of the signing operation's configuration.
An incomplete paired request must fail validation.
Grant future spending authority separately
A delegation policy checks the proposed permissions for future purchases. The application enforces those permissions when later payments occur. For open checkout/payment pairs, use a rule such as:
allow:
- id: bounded-delegation
where:
- "action.kind == 'delegation'"
- "delegation.merchantsLimitedTo([merchants.officeShop])"
- "delegation.payeesLimitedTo([merchants.officeShop])"
- "delegation.methodsLimitedTo([methods.companyCard])"
- "delegation.maxAmountAtMost('100.00', 'USD')"
- >-
delegation.hasOnlyConstraints([
'checkout.allowed_merchants', 'checkout.line_items',
'payment.reference', 'payment.allowed_payees',
'payment.allowed_payment_instruments', 'payment.amount_range'
])
This is a replacement policy fragment for a delegation-specific authority. Keep
fallback: DENY and the identity catalogs. Remove the purchase example's
!request.totalAtMost(...) deny rule: delegations are unpriced and fail that check.
| Function | Question it answers |
|---|---|
merchantsLimitedTo([...]) |
Is there an explicit merchant restriction, with every candidate allowed here? |
payeesLimitedTo([...]) |
Are all fixed/allowed payment recipients within these definitions? |
methodsLimitedTo([...]) |
Are all fixed/allowed payment methods within these definitions? |
maxAmountAtMost('100.00', 'USD') |
Is there an explicit fixed amount or maximum no greater than USD 100? |
hasOnlyConstraints([...]) |
Are all constraint types in this list? |
Missing bounds return false. The lists must be nonempty. hasOnlyConstraints
checks the types of constraints present; use bound helpers to require specific limits.
To restrict products, also check the values inside checkout.line_items.
This fragment limits identities and per-payment amounts. The application handles
cumulative spending, recurrence, fulfillment, replay and delegation-chain verification.
Java integration
Create an evaluator with the payment functions, compile the authority policy, and build an intent from all mandate content belonging to the signing request:
import java.util.List;
import org.exploit.verdict.PolicyEvaluator;
import org.exploit.verdict.intent.ap2.Ap2Intent;
import org.exploit.verdict.intent.payment.PaymentRequest;
import org.exploit.verdict.intent.payment.config.PaymentIntentConfig;
import org.exploit.verdict.intent.payment.function.PaymentFunctions;
import org.exploit.verdict.intent.payment.model.MandateInput;
var functions = new PaymentFunctions();
var evaluator = PolicyEvaluator.builder().library(functions, functions).build();
var config = PaymentIntentConfig.fromMap(authority.config());
var policy = evaluator.compileStrict(authority.policy(), PaymentRequest.policySchema());
var request = Ap2Intent.request(List.of(
MandateInput.checkout(checkoutMandateJson, decodedCheckoutJson),
MandateInput.of(paymentMandateJson)), config);
var result = evaluator.evaluate(policy, request);
authority is a loaded authority document.
checkoutMandateJson, paymentMandateJson, and decodedCheckoutJson are Jackson
JsonNode objects. Pass the trees from your application's JSON decoder directly.
decodedCheckoutJson contains the checkout associated with the checkout mandate.
The adapter establishes that association and handles all cryptography.
For Mastercard VI, use McIntent.request from
org.exploit.verdict.intent.mcintent with the same input and config types.
The default mode expects checkout/payment pairs. Include every mandate being signed;
Verdict groups pairs by protocol references and rejects orphaned or duplicate members.
For an open pair, use MandateInput.openCheckout(openCheckoutJson, checkoutDisclosureHash)
plus MandateInput.of(openPaymentJson). The adapter supplies the actual disclosure
hash; Verdict compares the supplied references without computing or verifying hashes.
For a payment-only signing operation, select the mode explicitly:
var request = Ap2Intent.request(
List.of(MandateInput.of(paymentMandateJson)),
PaymentRequestMode.PAYMENTS, config);
Import org.exploit.verdict.intent.payment.constant.PaymentRequestMode.
CHECKOUTS similarly accepts closed checkout mandates only. Standalone open mandates
are not supported by these modes.
MandateInput also works with Jackson deserialization. For example, a payment input
for PAYMENTS mode has this JSON representation:
{
"json": {
"vct": "mandate.payment.1",
"transaction_id": "order-1",
"payee": {"id": "merchant-1", "name": "Office shop", "website": "https://office.example"},
"payment_instrument": {"id": "card-1", "type": "card"},
"payment_amount": {"currency": "USD", "amount": 7500}
},
"checkoutJson": null,
"disclosureHash": null
}
Use mapper.treeToValue(inputNode, MandateInput.class) to construct the input.
For a closed checkout mandate, checkoutJson contains the decoded checkout object.
Both JSON fields are copied on construction and access, so changes to a caller's
tree do not change retained inputs or the evaluated request.
Enable StreamReadFeature.STRICT_DUPLICATE_DETECTION and
DeserializationFeature.FAIL_ON_TRAILING_TOKENS on the application's JSON decoder.
Enable DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS to preserve decimal values.
Duplicate keys and numeric precision already lost during decoding cannot be recovered
from a JsonNode.
Reuse the evaluator across authorities and compile once per authority revision.
Pass trusted additional data through request.withContext(context); it is exposed
under the context root. Evaluate the returned request through the same API.
For multiple actions, rule matches and approval sources include an action[i]/
prefix. request.actions().get(i).mandateIndices() identifies their original inputs.
Retain request.inputs() and sign exactly the evaluated content after satisfying
any approval requirements.
Where to go next
- Core API: compilation, evaluation and custom functions.
- AP2 input format or VI input format: protocol-specific reader behavior.
- Troubleshooting: missing catalog keys, library registration and failed conditions.