Every payment system has disputes. A customer says they did not make a purchase, or that what arrived is not what they ordered, or that they were charged twice. With an agent in the loop, a new question sits underneath all of these: did the user actually authorise this, or did the agent go beyond what it was allowed to do? The Agent Payments Protocol (AP2) is designed so that this question has a cryptographic answer, and the practical work of dispute handling is turning that answer into evidence a merchant, processor or network can rely on.

This article covers the mechanics in the current version of the specification. AP2 v0.1, published in September 2025, described Intent, Cart and Payment mandates; v0.2, released in April 2026, restructures the protocol around a Checkout Mandate and a Payment Mandate, each in an open and a closed form, secured as SD-JWT verifiable credentials. The general argument that a mandate is evidence of authorisation, and the question of liability, are covered in AP2 chargebacks. Here the focus is how the evidence is built, joined, verified and stored, and how to design a dispute pipeline around it. The specification states that dispute resolution processes, retention and retrieval are outside its scope, so everything beyond the verification rules below is recommended design, labelled as such.

Advertisement

The roles and the artifacts

AP2 v0.2 names five roles. The Shopping Agent discovers products, builds the checkout and executes the purchase. The Credential Provider supplies the payment credential and checks that this agent may use it. The Merchant provides and completes the checkout and is responsible for inventory and pricing. The Merchant Payment Processor processes the payment and checks that the credential was authorised for this checkout. The Trusted Surface is a user interface, required to be non-agentic, that obtains informed consent and produces user-signed mandates. One company can play several roles.

ArtifactCreated byWhat it bindsHeld by (per spec)
Checkout JWTMerchantThe exact checkout: items, prices, totalsInside the Checkout Mandate
Checkout MandateTrusted Surface or Shopping Agentcheckout_jwt plus checkout_hash, its digestShopping Agent, Merchant
Checkout ReceiptMerchantreference to the closed Checkout Mandate, status, order_idShopping Agent, Merchant
Payment MandateTrusted Surface or Shopping Agenttransaction_id, payee, payment_amount, payment_instrumentShopping Agent, Credential Provider, Network, MPP
Payment ReceiptMerchant Payment Processorreference to the closed Payment Mandate, payment_id, confirmation IDsShopping Agent, Credential Provider, Network, MPP

The split is deliberate. Selective disclosure means the payment side never needs to see the basket and the merchant never needs to see more of the payment instrument than necessary. The two halves are designed to be joined only when a dispute requires it, and the join key is a hash.

How the pieces join

AP2 v0.2 evidence: two mandates, two receipts, joined by hashesCheckout JWTmerchant-signed checkoutCheckout Mandatecheckout_jwt + checkout_hashCheckout Receiptreference = hash of mandatePayment Mandatetransaction_id, payee, amountPayment Receiptreference, payment_idOpen mandatesuser-signed constraintsDispute verifierrecomputes every hashhashed intobound bybound bycheckout_hash = transaction_idsd_hash (autonomous only)Held by: Shopping Agent, Merchant (checkout side); Shopping Agent, Credential Provider, Network, MPP (payment side).
The Checkout Mandate carries a digest of the merchant's Checkout JWT; the Payment Mandate's transaction_id is that same digest; each receipt references the digest of the closed mandate it answers.

Three hash bindings hold the bundle together. The Checkout Mandate's checkout_hash is the base64url digest of its checkout_jwt value, using the SD-JWT's _sd_alg if present and SHA-256 otherwise. The Payment Mandate's transaction_id is the digest of the same Checkout JWT, so a payment cannot be moved to a different basket. And each receipt's reference is the digest of the closed mandate it responds to, computed the same way SD-JWT computes sd_hash, so a receipt cannot be reattached to a different mandate.

One further detail protects privacy: because the Checkout JWT is signed with a randomised signature scheme such as ECDSA, its digest cannot be reversed by guessing likely baskets. The specification forbids deterministic schemes such as Ed25519 here unless a high-entropy salt is added. In autonomous mode there is a fourth binding: each closed mandate carries an sd_hash tying it to the specific user-signed open mandate it was derived from.

Advertisement

The five checks at dispute time

The specification lists the steps a verifier must perform before the bundle can be used as evidence of what the user and each role saw. The sketch below follows them in order. The SD-JWT functions are placeholders for whichever library you use, not a real API.

import base64, hashlib

def b64url_digest(data: bytes, alg: str = "sha-256") -> str:
    h = hashlib.new(alg.replace("-", ""), data).digest()
    return base64.urlsafe_b64encode(h).rstrip(b"=").decode()

def verify_dispute_bundle(b, trust) -> dict:
    """b holds the exact serialized artifacts as received, never re-encoded.

    verify_sd_jwt() and sd_hash_of() are placeholders for your SD-JWT library:
    signature, disclosure and key-binding checks per the AP2 agent authorization
    rules, and the digest computed the way SD-JWT computes sd_hash.
    """
    checkout = verify_sd_jwt(b["checkout_mandate"], trust)          # 1. merchant rules
    alg = checkout.get("_sd_alg", "sha-256")
    computed = b64url_digest(checkout["checkout_jwt"].encode("ascii"), alg)
    if computed != checkout["checkout_hash"]:                        # 2. recompute
        raise Invalid("checkout_hash does not match checkout_jwt")
    verify_merchant_signature(checkout["checkout_jwt"], trust)
    if open_mandates := b.get("open_checkout_mandates"):
        check_constraints(checkout, open_mandates)                   # autonomous mode

    creceipt = verify_jwt(b["checkout_receipt"], trust)
    if creceipt["reference"] != sd_hash_of(b["checkout_mandate"]):   # 3. receipt binds
        raise Invalid("checkout receipt is for a different mandate")

    payment = verify_sd_jwt(b["payment_mandate"], trust)             # 4. MPP rules
    if payment["transaction_id"] != checkout["checkout_hash"]:
        raise Invalid("payment mandate is for a different checkout")
    if open_mandates := b.get("open_payment_mandates"):
        check_constraints(payment, open_mandates)

    preceipt = verify_jwt(b["payment_receipt"], trust)
    if preceipt["reference"] != sd_hash_of(b["payment_mandate"]):    # 5. receipt binds
        raise Invalid("payment receipt is for a different mandate")

    return {"checkout": checkout, "payment": payment,
            "order_id": creceipt.get("order_id"), "payment_id": preceipt["payment_id"]}

In words: verify the Checkout Mandate exactly as a merchant would at purchase time; independently recompute the hash of the included Checkout JWT rather than trusting the stated one; check that the Checkout Receipt references this mandate; verify the Payment Mandate as the processor would, using the checkout_hash from the Checkout Mandate; and check that the Payment Receipt references this Payment Mandate. Only when all five pass does the bundle show, non-repudiably, which checkout the user or agent approved and which payment was made for it.

Be careful with schema versions. Mandates identify their schema with a vct claim, and the specification requires an exact match including a numeric version suffix; at the time of writing the prose documents and the JSON schema descriptions in the repository show the value slightly differently. Match against what your counterparties actually emit, and keep the verifier version-aware.

Store the bytes, not the meaning

The most important operational rule for AP2 evidence is that every check above is a hash over exact bytes. If you parse a mandate into your order database and later re-serialise it, a changed field order, whitespace or encoding produces a different digest and every check fails. The evidence becomes worthless at exactly the moment you need it. Store each artifact exactly as received, alongside a digest of the stored copy and the result of verifying it at receipt time.

CREATE TABLE ap2_evidence (
  transaction_id   TEXT        NOT NULL,   -- = checkout_hash; the join key
  artifact_kind    TEXT        NOT NULL,   -- checkout_mandate | checkout_receipt |
                                           -- payment_mandate  | payment_receipt |
                                           -- open_checkout_mandate | open_payment_mandate
  raw              BYTEA       NOT NULL,   -- exact bytes received; never re-serialized
  raw_sha256       TEXT        NOT NULL,   -- integrity check for the stored copy
  received_from    TEXT        NOT NULL,   -- role and endpoint that sent it
  received_at      TIMESTAMPTZ NOT NULL,
  verified_ok      BOOLEAN     NOT NULL,   -- result of verification at receipt time
  verifier_version TEXT        NOT NULL,
  PRIMARY KEY (transaction_id, artifact_kind, raw_sha256)
);
-- Plus a key archive: every merchant, user, agent and receipt-issuer public key
-- that verified these artifacts, retained for as long as the evidence itself.

Two things are easy to forget. Signatures are only verifiable with the public keys that were valid at the time, so archive every merchant, user, agent and receipt-issuer key used, together with the trust list you checked it against. And the join key should be transaction_id, because it is the one value both sides hold. The specification suggests that a future version may let parties fetch the Checkout Mandate from the Shopping Agent or Merchant using that ID; until then, retrieval between parties is by bilateral arrangement. Retention periods are your decision and your regulators'; keep evidence at least as long as a dispute can be raised on the payment method involved. Posting the financial effects of a dispute is covered in double-entry ledger architecture for agent payments.

What the evidence proves, and what it does not

Dispute claimEvidence that answers itLimit
I never authorised thisSignature on the closed mandates: the user's key in direct mode; in autonomous mode the agent's key plus a user-signed open mandate naming that key in cnfProves the key signed; account takeover of the key is a separate investigation
The agent exceeded what I allowedEvaluate each disclosed constraint, such as amount range, allowed payees, budget and recurrence, against the closed mandatesOnly constraints the user actually set can be checked
That is not what I orderedThe merchant-signed Checkout JWT shows the exact items and prices approvedSays nothing about what was delivered
I was charged more than approvedCompare payment_amount with the checkout total and the processor's settled amountCurrency conversion and fees need their own records
I was charged twiceTwo Payment Receipts for one transaction_id, or overlapping closed mandates from one open mandate beyond what its recurrence constraint allowsRecurrence legitimately yields several closed mandates; count them against the constraint

The pattern matters for triage. Authorisation disputes are largely decidable by code from the bundle. Quality and delivery disputes are not: the bundle proves what was ordered, and the merchant's fulfilment records must prove what was delivered. A good pipeline classifies early and sends each class down the path that can actually resolve it.

Worked example: an autonomous purchase disputed

A user asks their agent to reorder printer ink when stock runs low, spending at most 60 dollars per order, only from two named merchants, at most once a month. The Trusted Surface produces open Checkout and Payment Mandates with those constraints, the agent's public key in cnf and an exp just long enough to cover the year of monthly orders. Weeks later the agent buys a 58-dollar ink bundle from one of the named merchants, signing closed mandates with its own key. The merchant verifies the closed Checkout Mandate against the open one and returns a Success receipt; the processor verifies the payment and returns a Payment Receipt.

A month later the user disputes the charge: they did not expect to buy anything. The merchant's dispute service looks up the transaction_id from the processor's dispute notification, loads the stored artifacts, and runs the five checks plus constraint evaluation. The bundle shows a user-signed open mandate authorising this agent key, a closed payment of 58 dollars to an allowed payee, within the amount range and the monthly recurrence. The merchant submits that as evidence of authorisation. Had the agent paid 75 dollars, the amount constraint would fail and the merchant should accept the dispute rather than contest it: the evidence shows the payment went beyond the user's mandate, and that some verifier skipped the constraint check at purchase time.

A recommended dispute pipeline

  1. Intake. Receive the dispute from the processor or network with its payment identifiers; resolve the transaction_id through the Payment Receipt's payment_id.
  2. Assemble. Load all artifacts for that ID from the evidence store; if the checkout half is missing, request it from the Shopping Agent under whatever agreement you have.
  3. Verify. Run the five checks and any constraint evaluation with the archived keys; record the verifier version and result.
  4. Classify. Authorisation, scope, merchandise, amount or duplicate, using the table above.
  5. Decide. Accept automatically when the evidence shows the payment was outside the mandate; contest automatically when it is inside and the claim is about authorisation; route quality and delivery claims to people with fulfilment data.
  6. Respond and post. Submit the evidence within the deadline, and post provisional and final ledger entries; a lost dispute should reverse through the same path as a refund.

Model each case as a state machine with explicit deadlines, because the network's response window, not your queue, sets the pace. Keep the automation's decision and its evidence together in the case record, so that a human reviewing an appeal can see why the system contested.

Failure modes

  • Re-serialised artifacts. Every hash fails. Store raw bytes and test the verifier against stored copies, not in-memory objects.
  • Missing half. The processor holds the Payment Mandate but nobody kept the Checkout Mandate. Retain both, or agree retrieval with your agent partners in advance.
  • Rotated keys. The merchant rotated its signing key and the old public key was not archived; the Checkout JWT can no longer be verified.
  • Trusting stated hashes. Comparing checkout_hash with transaction_id without recomputing from checkout_jwt lets a forged pair through.
  • Over-disclosure. Sending every open-mandate disclosure to a dispute counterpart leaks user intent the spec deliberately hid; disclose only what the decision needs.
  • Version skew. A verifier that rejects a newer vct suffix fails closed on valid evidence; keep verifiers updated with counterparties.

What to do next

  1. Map which AP2 roles your system plays and which artifacts each role receives.
  2. Store every mandate and receipt as the exact bytes received, keyed by transaction_id, with a digest and a verification result.
  3. Archive every public key and trust list used to verify them for the full retention period.
  4. Implement the five dispute-time checks, recomputing hashes rather than trusting stated ones, and add constraint evaluation for autonomous mandates.
  5. Test the verifier on stored artifacts from real transactions, including rotated keys.
  6. Classify disputes by claim type and automate accept or contest only for authorisation and scope claims.
  7. Agree retrieval of the missing half of the bundle with your agent and merchant partners before the first dispute arrives.
  8. Review AP2 mandate architecture and AP2 receipts to align purchase-time verification with dispute-time verification.
Key takeaway: AP2 turns the question at the heart of an agent-payment dispute, did the user authorise this, into something code can check. In v0.2 the evidence is two SD-JWT mandates and two receipts joined by hashes: checkout_hash equals transaction_id, and each receipt references the digest of its closed mandate. Verify all five bindings with archived keys, recompute every hash, and evaluate open-mandate constraints in autonomous mode. Above all, store the artifacts as the exact bytes received, because a re-serialised mandate is no longer evidence. Automate authorisation and scope disputes, and route merchandise disputes to fulfilment data.