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.

An escalated purchase
{
  "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
}
OutcomeMeaningBudget
APPROVEDPermitted under the rules in force.Committed
REJECTEDRefused. The explanation says why.Nothing reserved
REQUIRES_APPROVALA 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#

CodeMeaning
WITHIN_POLICYEvery rule that applied was satisfied.
AGENT_DISABLEDThe 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_POLICYNo policy that governs this agent is about this purchase, so it has no spending authority. Veriticity approves nothing by default.
NO_APPLICABLE_RULEPolicies govern this agent, but none of their rules applies to this purchase, so nothing grants it.
POLICY_CHANGED_SINCE_ESCALATIONA person approved it, but the policies changed before it was re-evaluated, and it no longer passes.
POLICY_MALFORMEDA policy version could not be read, so everything it governs is refused.
RULE_MALFORMEDA rule could not be read, so it refuses.
FACT_UNVERIFIEDA rule reads something the agent stated and nobody has verified, such as the merchant, so a person has to confirm it.
FACT_UNAVAILABLEA rule reads something this purchase did not say.
FACT_STALEA rule reads something that was verified too long ago to rely on.
MEASUREMENT_UNAVAILABLEA rule measures something that could not be measured.
CONDITION_ERRORA condition failed while it was being checked, so its rule refuses.
AMOUNT_OVER_LIMITThe amount is over a limit on a single purchase. The rule's effect says whether that refuses it or asks a person.
AMOUNT_UNKNOWNA rule limits the amount and the amount could not be read.
CURRENCY_NOT_COMPARABLEA 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_LIMITThis would take spending in a window, such as a day or a month, over its limit.
SPEND_NOT_COUNTED
SPEND_NOT_READA rule limits spending in a window and the total could not be read.
SPEND_NOT_ESTABLISHED
SCOPE_SUBJECT_NOT_ESTABLISHEDA 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_SETRecorded 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_SETA 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_UNKNOWNA rule reads the merchant and the purchase did not name one.
MERCHANT_IDENTITY_UNESTABLISHEDA 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_LISTEDPolicies govern this agent, but none sets limits in the currency of this purchase. Veriticity performs no currency conversion.
CURRENCY_UNKNOWNA rule reads the currency and it could not be read.
MERCHANT_NOT_VERIFIEDA rule requires a verified merchant, and this one is not currently verified.
MERCHANT_LISTEDA rule requires the merchant not to be one of those it lists, and it is.
MERCHANT_NOT_LISTEDA rule requires the merchant to be one of those it lists, and it is not.
MERCHANT_COUNTRY_LISTEDA rule excludes the merchant's country.
MERCHANT_COUNTRY_NOT_LISTEDA rule lists the countries a merchant may be in, and this one is elsewhere.
MERCHANT_COUNTRY_UNESTABLISHEDA rule reads the merchant's country and none is on record.
LEGAL_ENTITY_JURISDICTION_LISTEDA rule excludes the country the merchant's company is registered in.
LEGAL_ENTITY_JURISDICTION_NOT_LISTEDA rule lists where a merchant's company may be registered, and this one is registered elsewhere.
LEGAL_ENTITY_UNESTABLISHEDA rule reads the company behind the merchant and none is on record.
PRODUCT_IDENTITY_UNESTABLISHEDA rule reads the product, and the product reference on this purchase is not on record for this merchant.
PRODUCT_CATEGORY_LISTEDA rule excludes this product's category, or one it is inside.
PRODUCT_CATEGORY_NOT_LISTEDA rule lists the categories allowed, and this product is in none of them.
PRODUCT_CATEGORY_UNESTABLISHEDA rule reads the product's category and none is on record.
PRODUCT_ATTRIBUTE_LISTEDA rule excludes a detail of this product, such as its condition.
PRODUCT_ATTRIBUTE_NOT_LISTEDA rule lists the values a detail of the product may have, and this one has another.
PRODUCT_ATTRIBUTE_UNESTABLISHEDA 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 is NOT_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 to EXPIRED within a few minutes of the deadline, so a poll immediately after it may still read PENDING_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.

StatusMeaning
activeCan be used until expiresAt.
usedUsed once. It can’t be used again.
expiredCan’t be used. It is not renewed: request the purchase again.
cancelledCan’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:

Using an authorization with the agent's key
# 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.
resultMeaning
authorizedThe terms match and the authorization has been used. Go ahead. It can’t be used again.
refusedDon’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.

  • authorized means 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 with replayed: true. A different reference is refused with already_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.

CallWhen
POST /v1/purchase-authorizations/{authorizationId}/withdrawThe authorization has not been used. It may be active or expired. It can’t be used afterwards.
POST /v1/purchase-authorizations/{authorizationId}/voidThe authorization was used and the purchase did not go through. Voiding does not refund or reverse a payment.
POST /v1/purchase-requests/{purchaseRequestId}/cancelThe 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_conflict and changes nothing.
  • Repeating a call that already succeeded returns the same answer with replayed: true.
  • Each of these sends purchase.cancelled to 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 returns replayed: true; a different report under a used reportId is report_id_reuse. A purchase that was never approved is purchase_outcome_conflict.
  • The answer’s purchase names the purchase the report was recorded against: its merchant, merchantDomain, amountMinor, maximumMinor, currency and requestedAt.

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 sendYou get
Same key, same body200 with the original decision, the same purchase authorization and replayed: true. No new budget reserved.
Same key, different body409 idempotency_key_reuse
Same key, same body, a different credential409 idempotency_key_reuse. A retry is answered only for the credential that made the request.
No key400 invalid_request, with an issue at Idempotency-Key. Nothing is evaluated.
Retrying safely
// 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.

EndpointReturns
GET /v1/agentThe agent, and the currency and timezone its organisation operates in.
GET /v1/spending-rulesThe policies in force for it, with their rules, and the size of any merchant list.
GET /v1/budget-statusEach 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.

FieldRequiredNotes
merchantYesAs a person would write it. Normalised for matching.
amountMinorYesMinor units, as a string. "7500" is GBP 75.00.
reasonYesWhy the agent wants it. Every purchase must say.
currencyNoDefaults to the organisation’s own currency.
merchantDomainNoThe 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.
merchantReferenceNoA commerce platform’s id for the merchant, as { provider, id }.
productReferenceNoAn 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.
categoryUnless itemsWhat 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.
itemsInstead of categoryUp 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.
chargesNoUp 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.
taxIncludedWith itemsWhether the item prices already include tax. When they do, tax isn’t stated again as a charge.
metadataNoAny 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.