Purchases
Submit a purchase for a decision, check one before committing, and follow an approval escalation to its outcome. Decisions, reason codes, idempotency and budget holds.
A purchase request asks the Trust Engine one question: may this agent buy this thing, right now? The answer is a decision, and it is always explained.
The shape of a decision#
One response shape for all three outcomes. A client reads decision.outcome and branches; it never has to parse prose or infer meaning from a status code.
{
"purchaseRequest": {
"id": "0199a3f1-8c2e-7b40-9f31-6d2e5a1c0b77",
"status": "PENDING_APPROVAL",
"amountMinor": "42000",
"maximumMinor": null,
"currency": "GBP"
},
"decision": {
"id": "0199a3f1-91b4-7c02-a55d-3e7f1d9c4a20",
"outcome": "REQUIRES_APPROVAL",
"authority": null,
"reasonCodes": ["AMOUNT_OVER_LIMIT"],
"explanation": "Requires approval. GBP 420.00 is over the GBP 250.00 limit for a single purchase.",
"evaluatedAt": "2026-09-12T09:14:07.882Z",
"engineVersion": "veriticity-trust-engine@0.1.0"
},
"approval": {
"required": true,
"expiresAt": "2026-09-13T09:14:07.882Z"
},
"policyVersions": [
{ "policyVersionId": "0199a3e0-…", "applicabilityReason": "Bound to the organization" }
],
"findings": [
{
"policyId": "0199a3e0-…",
"policyName": "Company baseline",
"policyVersionId": "0199a3e0-…",
"policyVersionNumber": 4,
"policyRuleId": "0199a3e0-…",
"ruleKey": "approval-threshold",
"rule": "Approval threshold",
"outcome": "REQUIRE_APPROVAL",
"role": "BINDING",
"limitMinor": "25000",
"observedMinor": "42000",
"headroomMinor": "-17000",
"currency": "GBP",
"reasonCode": "AMOUNT_OVER_LIMIT",
"message": "GBP 420.00 is over the GBP 250.00 limit for a single purchase."
},
{
"policyId": "0199a3e0-…",
"policyName": "Company baseline",
"policyVersionId": "0199a3e0-…",
"policyVersionNumber": 4,
"policyRuleId": "0199a3e0-…",
"ruleKey": "max-purchase",
"rule": "Maximum purchase amount",
"outcome": "PASS",
"role": "SATISFIED",
"limitMinor": "50000",
"observedMinor": "42000",
"headroomMinor": "8000",
"currency": "GBP",
"reasonCode": "REQUIREMENT_MET",
"message": "GBP 420.00 is within the GBP 500.00 limit for a single purchase."
}
],
"replayed": false
}| Outcome | Meaning | Budget |
|---|---|---|
APPROVED | Permitted under the rules in force. | Committed |
REJECTED | Refused. The explanation says why. | Nothing reserved |
REQUIRES_APPROVAL | A person has been asked. | Held until approval.expiresAt |
APPROVED means Veriticity permits the purchase. Veriticity does not make the purchase and is not told whether it happened. A spending limit counts every purchase that is approved or waiting for approval, by the agents its policy covers, whether or not the money was spent.
findings lists every rule considered, including the ones that passed and the ones that did not apply. That is deliberate: “why was this approved” needs the passes as much as “why was this refused” needs the failures. Each finding names its rule by ruleKey. On an approved purchase, decision.authority says whether it was WITHIN_POLICY or AUTHORIZED by a person.
purchaseRequest.amountMinor is the known amount. When a charge can still vary, purchaseRequest.maximumMinor is the most the purchase can cost, with every such charge at its maximum, and is what rules and budgets judged. It is null when no charge can vary. A purchase you read or list carries both, and a simulation’s purchase does too.
explanation is a sentence written for a person to read, and it may be reworded. Branch on reasonCodes, which are stable.
Reason codes#
| Code | Meaning |
|---|---|
WITHIN_POLICY | Every rule that applied was satisfied. |
AGENT_DISABLED | The agent has been switched off. The purchase is refused by the engine rather than at the door, so the attempt is still recorded. |
NO_APPLICABLE_POLICY | No policy that governs this agent is about this purchase, so it has no spending authority. Veriticity approves nothing by default. |
NO_APPLICABLE_RULE | Policies govern this agent, but none of their rules applies to this purchase, so nothing grants it. |
POLICY_CHANGED_SINCE_ESCALATION | A person approved it, but the policies changed before it was re-evaluated, and it no longer passes. |
POLICY_MALFORMED | A policy version could not be read, so everything it governs is refused. |
RULE_MALFORMED | A rule could not be read, so it refuses. |
FACT_UNVERIFIED | A rule reads something the agent stated and nobody has verified, such as the merchant, so a person has to confirm it. |
FACT_UNAVAILABLE | A rule reads something this purchase did not say. |
FACT_STALE | A rule reads something that was verified too long ago to rely on. |
MEASUREMENT_UNAVAILABLE | A rule measures something that could not be measured. |
CONDITION_ERROR | A condition failed while it was being checked, so its rule refuses. |
AMOUNT_OVER_LIMIT | The amount is over a limit on a single purchase. The rule's effect says whether that refuses it or asks a person. |
AMOUNT_UNKNOWN | A rule limits the amount and the amount could not be read. |
CURRENCY_NOT_COMPARABLE | A limit is set in one currency and the purchase is in another. Veriticity performs no currency conversion. |
AMOUNT_AT_OR_OVER_MINIMUM | |
AMOUNT_UNDER_MINIMUM | |
AMOUNT_MINIMUM_NOT_ESTABLISHED | |
SPEND_OVER_LIMIT | This would take spending in a window, such as a day or a month, over its limit. |
SPEND_NOT_COUNTED | |
SPEND_NOT_READ | A rule limits spending in a window and the total could not be read. |
SPEND_NOT_ESTABLISHED | |
SCOPE_SUBJECT_NOT_ESTABLISHED | A limit names an agent, person, merchant or category the organisation does not have, so what it limits cannot be established. |
PURCHASE_COUNT_WITHIN_LIMIT | |
PURCHASE_COUNT_OVER_LIMIT | |
PURCHASE_COUNT_NOT_COUNTED | |
PURCHASE_COUNT_NOT_READ | |
PURCHASE_COUNT_NOT_ESTABLISHED | |
MERCHANT_IN_SET | Recorded on earlier decisions: a rule required the merchant not to be on a list of typed names, and it was. No longer produced. |
MERCHANT_NOT_IN_SET | A rule reads a list of typed merchant names, and the purchase is with a merchant on record. A name identifies no merchant, so the merchant is not on the list. With `reasonParams.state`, the name is on the list and the rule refuses whichever way it reads. A purchase with no merchant on record gets `MERCHANT_IDENTITY_UNESTABLISHED` instead. |
MERCHANT_UNKNOWN | A rule reads the merchant and the purchase did not name one. |
MERCHANT_IDENTITY_UNESTABLISHED | A rule reads the merchant, and the domain or reference on this purchase is not on record for one merchant, or the purchase states neither. `reasonParams.state` says whether it is unknown, points to more than one merchant, or is out of date. |
CURRENCY_NOT_LISTED | Policies govern this agent, but none sets limits in the currency of this purchase. Veriticity performs no currency conversion. |
CURRENCY_UNKNOWN | A rule reads the currency and it could not be read. |
MERCHANT_NOT_VERIFIED | A rule requires a verified merchant, and this one is not currently verified. |
MERCHANT_LISTED | A rule requires the merchant not to be one of those it lists, and it is. |
MERCHANT_NOT_LISTED | A rule requires the merchant to be one of those it lists, and it is not. |
MERCHANT_COUNTRY_LISTED | A rule excludes the merchant's country. |
MERCHANT_COUNTRY_NOT_LISTED | A rule lists the countries a merchant may be in, and this one is elsewhere. |
MERCHANT_COUNTRY_UNESTABLISHED | A rule reads the merchant's country and none is on record. |
LEGAL_ENTITY_JURISDICTION_LISTED | A rule excludes the country the merchant's company is registered in. |
LEGAL_ENTITY_JURISDICTION_NOT_LISTED | A rule lists where a merchant's company may be registered, and this one is registered elsewhere. |
LEGAL_ENTITY_UNESTABLISHED | A rule reads the company behind the merchant and none is on record. |
PRODUCT_IDENTITY_UNESTABLISHED | A rule reads the product, and the product reference on this purchase is not on record for this merchant. |
PRODUCT_CATEGORY_LISTED | A rule excludes this product's category, or one it is inside. |
PRODUCT_CATEGORY_NOT_LISTED | A rule lists the categories allowed, and this product is in none of them. |
PRODUCT_CATEGORY_UNESTABLISHED | A rule reads the product's category and none is on record. |
PRODUCT_ATTRIBUTE_LISTED | A rule excludes a detail of this product, such as its condition. |
PRODUCT_ATTRIBUTE_NOT_LISTED | A rule lists the values a detail of the product may have, and this one has another. |
PRODUCT_ATTRIBUTE_UNESTABLISHED | A rule reads a detail of the product and it is not on record. |
ATTEMPT_COUNT_WITHIN_LIMIT | |
ATTEMPT_COUNT_OVER_LIMIT | |
ATTEMPT_COUNT_NOT_COUNTED | |
ATTEMPT_COUNT_NOT_READ | |
HISTORY_NOT_ATTRIBUTABLE | |
DISTINCT_MERCHANTS_WITHIN_LIMIT | |
DISTINCT_MERCHANTS_OVER_LIMIT | |
DISTINCT_MERCHANTS_NOT_ESTABLISHED | |
DISTINCT_MERCHANTS_NOT_COUNTED | |
DISTINCT_MERCHANTS_NOT_READ | |
DISTINCT_CATEGORIES_WITHIN_LIMIT | |
DISTINCT_CATEGORIES_OVER_LIMIT | |
DISTINCT_CATEGORIES_NOT_ESTABLISHED | |
DISTINCT_CATEGORIES_NOT_COUNTED | |
DISTINCT_CATEGORIES_NOT_READ | |
PURCHASE_HISTORY_SUFFICIENT | |
PURCHASE_HISTORY_INSUFFICIENT | |
PURCHASE_HISTORY_NOT_READ | |
AMOUNT_WITHIN_BASELINE | |
AMOUNT_ABOVE_BASELINE | |
AMOUNT_NOT_COMPARED | |
BASELINE_NOT_ESTABLISHED | |
BASELINE_NOT_READ | |
ITEM_CATEGORY_INCLUDED | |
ITEM_CATEGORY_NOT_INCLUDED | |
ITEM_CATEGORY_ALL_LISTED | |
ITEM_CATEGORY_NOT_ALL_LISTED | |
ITEM_CATEGORY_NOT_ESTABLISHED | |
ITEM_CATEGORY_UNESTABLISHED | |
VARIABLE_COSTS_NONE | |
VARIABLE_COSTS_WITHIN_ALLOWANCE | |
VARIABLE_COST_NOT_PERMITTED | |
VARIABLE_COST_OVER_ALLOWANCE | |
VARIABLE_COSTS_OVER_CEILING | |
VARIABLE_COSTS_UNKNOWN | |
NOT_DECIDED_IN_ADVANCE |
Checking without committing#
POST /v1/simulations/current-policies runs the same evaluation and changes nothing — no purchase request, no decision, no budget reservation, no audit evidence, and no credential last-use stamp.
It returns more than a real decision does: every policy considered, every policy excluded for currency scope, and the budget windows with their current balances and what this purchase would do to them. It is the endpoint to use when an agent needs to decide whether to ask.
Rules about recent activity are not checked in advance. Those are rules on how many purchases were attempted, how many merchants or categories they span, how much purchase history there is, or how far an amount is from the usual one. They are decided when a purchase is requested. A simulation lists them in simulatedDecision.deferredRules, and simulatedDecision.advance says how much of the decision the answer is:
COMPLETE: every rule was checked. The outcome is what a request made now would be decided as.PARTIAL: some rules are decided on request, and the others already refuse. The refusal stands.NOT_DECIDED: some rules are decided on request, and the outcome isNOT_DECIDED_IN_ADVANCE. That is neither an approval nor a refusal. Request the purchase to find out.
Budgets are always checked, and a simulation still returns each budget window and what the purchase would leave in it.
An agent-bound credential simulates as itself and may not name an agentId. An organisation-scoped key must name one.
Approval escalations#
When a purchase crosses an approval threshold, the engine opens an escalation, holds the budget, and tells the people who can answer. Three things follow from that:
- The hold is real. The amount is reserved against the relevant budget windows while the purchase waits, so a second purchase cannot quietly spend the same headroom.
- The deadline is real. If nobody answers by
approval.expiresAt, the hold is released and the purchase will not happen unless it is submitted again. The status changes toEXPIREDwithin a few minutes of the deadline, so a poll immediately after it may still readPENDING_APPROVAL. - Approval is not a bypass. When a person approves, the purchase is re-evaluated against the policies in force at that moment. If the rules changed, the new rules apply — you may see
POLICY_CHANGED_SINCE_ESCALATION.
An agent cannot resolve its own escalation. There is no API for it and no MCP tool for it, by design: an assistant that could answer its own escalation would make the human half of the trust layer decorative.
Owners and admins approve and reject in the dashboard, including a purchase they requested themselves or that an assistant they connected requested. Users can request purchases and can’t approve or reject any. Viewers can do neither.
Purchase authorization#
An APPROVED purchase carries authorization: an id, a status and expiresAt. It is permission for that one purchase, on exactly the terms you submitted, until that time. Hand the id to whatever will make the purchase.
Veriticity authorizes a purchase. It does not make one, and an authorization is not a record that anything was bought.
| Status | Meaning |
|---|---|
active | Can be used until expiresAt. |
used | Used once. It can’t be used again. |
expired | Can’t be used. It is not renewed: request the purchase again. |
cancelled | Can’t be used. |
An expiry is never extended, and a retried submission returns the same authorization with the same expiry. A purchase that was not approved has authorization: null. So does a purchase approved before purchase authorizations were introduced: it stays approved, still counts towards your budgets, and can’t be used, withdrawn or voided. Nor can an authorization for a purchase made before its terms were recorded in full: it can still be withdrawn or voided.
Your agent reads the authorization on its own purchase, and can withdraw it while it is unused. Reading, listing and voiding authorizations, and withdrawing another’s, take an organization-scoped API key, which the dashboard does not issue. Your organisation’s members do these in the dashboard. The authorization endpoints return none of the purchase’s terms.
Using an authorization#
Before making the purchase, the system that will make it calls POST /v1/purchase-authorizations/{authorizationId}/use and says what it is about to do: the purchase’s terms exactly as it stated them, and its own executionReference. That is the amount, the currency, the merchant and its identifiers, and either the category or the items, charges and taxIncluded, in any order. When the purchase has charges that can still vary, variableFinal gives what each came to, by kind, and amountMinor is the known amount plus those finals.
Your agent can make this call with its own key. First it asks for an execution token for the authorization, then it sends that token with the use:
# 1. With the agent's key, ask for a token for the authorization.
curl -X POST https://app.veriticity.com/v1/purchase-authorizations/$AUTHORIZATION_ID/execution-token \
-H "Authorization: Bearer $VERITICITY_API_KEY"
# {"executionToken": {"token": "cmp_pex_...", "authorizationId": "...", "expiresAt": "..."}, "replaced": false}
# 2. Use the authorization, with the token in its own header.
curl -X POST https://app.veriticity.com/v1/purchase-authorizations/$AUTHORIZATION_ID/use \
-H "Authorization: Bearer $VERITICITY_API_KEY" \
-H "Veriticity-Execution-Token: $EXECUTION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"amountMinor": "42000", "currency": "GBP", "merchant": "Acme Cloud", "category": "software", "executionReference": "order-1182"}'- Only the key that submitted the purchase can get a token, and only while the authorization is
active. The token is shown once. - The token follows the authorization. It stops working when the authorization is used, withdrawn, voided or expires, whichever comes first. It can’t change the terms or the expiry, and it works for no other authorization.
- Asking again replaces the token. The earlier one is refused with
execution_token_invalid. If the response to a token request is lost, ask again. - An organization-scoped API key uses an authorization without a token. No OAuth connection can use one.
result | Meaning |
|---|---|
authorized | The terms match and the authorization has been used. Go ahead. It can’t be used again. |
refused | Don’t make the purchase. reason is one of expired, already_used, withdrawn, voided, not_usable, terms_mismatch, allowance_exceeded, execution_token_invalid or plan_required. plan_required means the organisation’s trial has ended or it has no plan: the authorization can be used once an owner chooses one, until it expires. |
Both answers are a 200. Branch on result, never on the HTTP status.
authorizedmeans the authorization was verified and used. It does not mean the purchase was made. Veriticity does not make the purchase and is not told its outcome.- Terms are compared exactly. The amount must match to the minor unit, and every identifier, item, charge and option the purchase stated must be presented and must match. A different item at the same total is refused. You are not told which term failed.
- A charge that can still vary is used within its maximum. A final above the maximum the purchase was approved for is refused with
allowance_exceeded: ask again for a new authorization. - A refusal changes nothing. The authorization is exactly as it was, however many times the wrong terms are presented.
- A retry is safe. Call again with the same
executionReference, the same terms, the same finals and the same token, and you get the original answer withreplayed: true. A different reference is refused withalready_used. - If the purchase fails after an
authorized, the authorization stays used and keeps counting towards budgets until your organisation voids it. If you don’t know whether the purchase went through, find out from the merchant before anything is voided or retried.
Ending an authorization#
An authorization that should no longer stand is ended by your organisation, from the dashboard or with an organization-scoped API key. The credential that submitted a purchase can also withdraw its authorization while it is unused, when the purchase is no longer wanted or its terms have changed. Each of these needs a reason, releases the purchase from your budgets, and can’t be undone.
| Call | When |
|---|---|
POST /v1/purchase-authorizations/{authorizationId}/withdraw | The authorization has not been used. It may be active or expired. It can’t be used afterwards. |
POST /v1/purchase-authorizations/{authorizationId}/void | The authorization was used and the purchase did not go through. Voiding does not refund or reverse a payment. |
POST /v1/purchase-requests/{purchaseRequestId}/cancel | The purchase is still waiting for approval. The agent that asked can call this with its own credential, and the reason is optional. |
- An expired authorization still counts towards budgets. Expiry releases nothing and sends no event. Withdraw the authorization to release it. To find them, call
GET /v1/purchase-authorizations?status=expired. - Calling the wrong one for the authorization’s state returns
409 purchase_authorization_conflictand changes nothing. - Repeating a call that already succeeded returns the same answer with
replayed: true. - Each of these sends
purchase.cancelledto your webhook endpoints.
Reporting what happened#
After an approved purchase has gone through, or hasn’t, the credential that submitted it says so with POST /v1/purchase-requests/{purchaseRequestId}/outcome: COMPLETED with finalAmountMinor, COMPLETED_AMOUNT_UNKNOWN, PARTIAL with the bought items’ itemPositions, FAILED, or REFUNDED with refundMinor. A purchase with charges that could vary can give each one’s final in variableFinal.
- A report is the agent’s word, and is shown as reported by the assistant. By default it waits for the purchaser or someone who can approve purchases to confirm what was charged. An organisation can choose to accept assistant reports instead.
- Until an outcome is accepted, the purchase is recorded at the most it was approved for. A report never approves anything, and never makes, changes or cancels an order or a payment.
- An amount above what was approved is recorded and flagged for the organisation to review. It counts until someone who can approve purchases confirms what was charged.
- A refund can be reported once the authorization has been used or withdrawn. Before then it is
purchase_outcome_conflict. - Send a
reportId. The same report again returnsreplayed: true; a different report under a usedreportIdisreport_id_reuse. A purchase that was never approved ispurchase_outcome_conflict. - The answer’s
purchasenames the purchase the report was recorded against: itsmerchant,merchantDomain,amountMinor,maximumMinor,currencyandrequestedAt.
Idempotency#
Idempotency-Key is a header, and POST /v1/purchase-requests is the only operation that accepts one. It is required: a submission without it is refused and nothing is created.
| You send | You get |
|---|---|
| Same key, same body | 200 with the original decision, the same purchase authorization and replayed: true. No new budget reserved. |
| Same key, different body | 409 idempotency_key_reuse |
| Same key, same body, a different credential | 409 idempotency_key_reuse. A retry is answered only for the credential that made the request. |
| No key | 400 invalid_request, with an issue at Idempotency-Key. Nothing is evaluated. |
// One key per logical purchase, derived from something stable in your own
// system. Not a random UUID per attempt -- that would make every retry a new
// purchase, which is the thing the header exists to prevent.
const idempotencyKey = `invoice-${invoice.id}`;
async function submit(): Promise<Decision> {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(purchase),
});
if (response.status === 409) {
const { error } = await response.json();
if (error.code === "idempotency_key_reuse") {
// The key was used for a *different* purchase. Retrying will never
// succeed; this is a bug in how the key was derived.
throw new Error("Idempotency key collision: " + error.message);
}
}
return response.json();
}Choosing a key#
Derive it from something stable in your own system — an invoice id, a job id, a cart id. A random value generated per attempt defeats the whole mechanism, because the retry carries a different key and becomes a second purchase.
A key is unique across your whole organisation, not per agent or per credential. Two agents that send the same key with different purchases collide: the second gets 409 idempotency_key_reuse. If several agents derive keys from the same kind of id, put the agent in the key.
Reading a purchase back#
GET /v1/purchase-requests/{id} returns a purchase with its decision and its approval, if one was opened.
A purchase is readable by the credential that submitted it and by nothing else. Another agent’s, another connection’s, another organisation’s and a malformed id are all the same 404 — distinguishing them would turn the endpoint into an oracle for which purchase ids exist.
Listing your purchases#
GET /v1/purchase-requests returns the purchases the calling credential submitted, newest first. Filter with status, since and until; page with limit (1 to 50, default 20) and the nextCursor from the previous page. It is the REST counterpart of MCP’s list_purchases.
What an agent can read about itself#
Three more reads for an agent-bound credential. None takes an agent id: a credential acts as one agent. Each answers the same as its MCP tool.
| Endpoint | Returns |
|---|---|
GET /v1/agent | The agent, and the currency and timezone its organisation operates in. |
GET /v1/spending-rules | The policies in force for it, with their rules, and the size of any merchant list. |
GET /v1/budget-status | Each budget window, whatever its length: spent, held for approvals, and remaining under each limit. |
spending-rules and budget-status take an optional ?currency=. Without it they answer in the organisation’s default currency. An organisation-scoped key is refused with api_key_not_bound_to_agent.
What a purchase body may contain#
Seven fields, and unknown fields are refused rather than ignored. A body containing agentId is not a request Veriticity can satisfy — it is a caller who believes they are choosing the agent, and silently dropping the field would leave them believing it.
| Field | Required | Notes |
|---|---|---|
merchant | Yes | As a person would write it. Normalised for matching. |
amountMinor | Yes | Minor units, as a string. "7500" is GBP 75.00. |
reason | Yes | Why the agent wants it. Every purchase must say. |
currency | No | Defaults to the organisation’s own currency. |
merchantDomain | No | The host you would buy from. It is what identifies the merchant: the name never does. It matches where the organisation has that host on record, with or without a leading www., or has chosen to trust the subdomains of a host it is under. A host that is sent and not on record leaves the merchant unrecognised, and so does sending none. |
merchantReference | No | A commerce platform’s id for the merchant, as { provider, id }. |
productReference | No | An id for the product, as { provider, id }. The provider is a platform, or a scheme such as sku or gtin. It is looked up within the merchant. |
category | Unless items | What kind of product it is, from Veriticity’s list: electronics, software, office-supplies, clothing, home-garden, food-drink, health-beauty, kids-baby, pet-supplies, sports-outdoors, automotive, travel, media-entertainment, services, medicines, gift-cards, alcohol, tobacco-nicotine, adult, gambling, crypto, recreational-drugs or other. Choose by what the product is, not where it’s sold. other is for a product you can identify that no category covers, not for one you can’t. Any other value is refused. A purchase holding different kinds of product, or a product that is two of medicines, gift-cards, alcohol, tobacco-nicotine, adult, gambling, crypto and recreational-drugs at once, states items instead. Each category’s definition, and where its neighbours go, is in the API reference. It is your claim, and Veriticity doesn’t check it: see Policies for how a rule treats it. |
items | Instead of category | Up to 50 items, each with a description, a category, a quantity and a unitAmountMinor, and optionally a discountMinor, a productUrl on the merchant’s website, a productReference and options. Tag an item with riskTags for every restricted kind it also involves: a crypto casino deposit is gambling tagged crypto, and a rule on either applies to it. If you can’t establish what an item is, send categoryUncertain: true and an uncertaintyReason instead of a category; never guess one. amountMinor must then be exactly the item lines plus the charges. Text limits are counted in bytes. |
charges | No | Up to 10 charges on the whole basket. KNOWN is an exact TAX, DELIVERY, DUTY, FEE or a negative DISCOUNT; a fee or discount says what it is. UNEXPLAINED is any part of the total you can’t account for. VARIABLE is a TAX, DELIVERY or DUTY charge that can still change, with maximumMinor, the most it may come to; amountMinor then leaves it out. The purchase is decided, and counts towards budgets, at the known amount plus every maximum. |
taxIncluded | With items | Whether the item prices already include tax. When they do, tax isn’t stated again as a charge. |
metadata | No | Any JSON object of at most 16 KiB, nested at most 8 levels. No rule reads it. |
A body that states items may be up to 640 KiB; any other purchase body up to 64 KiB. A body with items is decided item by item: a rule on a category applies to every item in it or tagged with it, and an item whose category isn’t established is never taken to be outside a rule or a budget. Its authorization is used only with exactly those items and charges.
The organisation and the agent are settled by the credential before the body is read at all. There is no code path by which a body field could reach either.

