Learn

AP2 Mandates in Practice: Credential Structure, Signing, and Verification

Andrew McPherson · Updated August 1, 2026

Depth · Advanced

Good for: Builders

AP2 answers one question: when a piece of software buys something for you, how does a merchant or bank get cryptographic proof that you authorized this specific purchase? The answer is the mandate, a signed, tamper-evident credential. This page is the practical companion to the conceptual overview: what a mandate actually contains, how it is signed and verified, and how the human-present and human-not-present flows differ in the data. It reflects AP2 v0.2, released on 28 April 2026 and still the current version.

One caveat before the detail: AP2 is still pre-production. The v0.2 release ships an SDK, JSON schemas, and runnable sample scenarios, but the public materials are reference implementations rather than documented live deployments. Build against the v0.2 schemas, and be aware the repository also contains an older model layer (intent and cart mandates) that should not be used for new work.

Two mandates, two states

AP2 v0.2 has two mandate types, and each exists in two states.

The Checkout Mandate is about what is being bought: the cart, the merchant, the terms. The Payment Mandate is about what will be charged: the amount, the instrument, the payee. Splitting them lets the parties who need each one see only that one.

The two states are the important part for agents. A closed mandate authorizes one specific action; it is the final, transaction-bound proof. An open mandate sets constraints on future actions an agent may take on the user’s behalf, for example a spending cap or an allowed list of merchants. Open mandates are what make a human-not-present purchase possible: the user signs the constraints once, and the agent later produces a closed mandate that must fall within them.

Each is identified by a vct (verifiable credential type) value: mandate.checkout.1 and mandate.checkout.open.1 for checkout, mandate.payment.1 and mandate.payment.open.1 for payment. The numeric suffix is a schema version number, and the specification requires implementations to match the exact vct string including that suffix, so a future incompatible revision would ship as .2 and simply fail to match rather than being silently misread.

What a closed mandate contains

A closed Checkout Mandate carries the merchant-signed checkout and a hash that uniquely identifies it:

{
  "vct": "mandate.checkout.1",
  "checkout_jwt": "<base64url merchant-signed JWT of the checkout payload>",
  "checkout_hash": "<base64url hash of checkout_jwt>",
  "iat": 1781200000,
  "exp": 1781203600
}

A closed Payment Mandate authorizes the charge and binds itself to that checkout by reusing its hash as the transaction_id:

{
  "vct": "mandate.payment.1",
  "transaction_id": "<the checkout_hash from above>",
  "payee": { "id": "M-1", "name": "Cat Store", "website": "https://catstore.example" },
  "payment_amount": { "amount": 27999, "currency": "USD" },
  "payment_instrument": { "id": "pi_123", "type": "card", "description": "Visa ending 4242" }
}

Two details matter for implementers. Amounts are integers in minor units, so 27999 means 279.99 dollars. And the link between the two mandates is cryptographic: the Payment Mandate’s transaction_id is the hash of the merchant-signed checkout_jwt, so a payment authorization cannot be detached from the exact checkout it was meant for.

What an open mandate adds: constraints

An open mandate replaces a specific action with a set of constraints plus a key-binding claim (cnf) that names the public key allowed to produce the eventual closed mandate. An open Payment Mandate might say “this agent may spend up to 50 dollars at these merchants”:

{
  "vct": "mandate.payment.open.1",
  "constraints": [
    { "type": "payment.amount_range", "currency": "USD", "max": 5000 },
    { "type": "payment.allowed_payees", "allowed": [ { "id": "M-1", "name": "Cat Store" } ] }
  ],
  "cnf": { "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } }
}

The constraint vocabulary is specific. The Payment Mandate defines eight constraint types: payment.amount_range, payment.budget, payment.allowed_payees, payment.allowed_payment_instruments, payment.allowed_pisps, payment.agent_recurrence (with a frequency from ON_DEMAND through ANNUALLY and an optional max_occurrences), payment.execution_date, and payment.reference, which pins a Payment Mandate to one particular open Checkout Mandate by digest. The Checkout Mandate defines two: checkout.allowed_merchants and checkout.line_items. A closed mandate that does not satisfy the open mandate’s constraints is invalid, and a verifier that meets a constraint type it does not recognize must treat it as failing rather than ignore it.

Signing: a chain of SD-JWTs

Mandates are not plain JWTs. They are SD-JWT verifiable credentials, which add two properties AP2 needs: selective disclosure, so a party sees only the fields it should, and key binding, so possession of a private key proves the right to act. Keys are EC P-256 and signatures are ES256.

One signing rule is easy to miss and worth getting right. The Payment Mandate binds itself to the checkout by hashing the merchant-signed checkout JWT, and that hash is only unguessable if the signature it covers carries entropy of its own. So the specification requires the checkout JWT to be signed with a non-deterministic scheme such as ECDSA rather than a deterministic one such as Ed25519, specifically to prevent rainbow-table attacks on the checkout contents. If you do use a deterministic scheme, the specification requires you to add a salt of sufficient entropy to the checkout instead.

Delegation is expressed as a chain of arbitrary depth. The root SD-JWT is signed by a root of trust, in AP2 typically the user’s bank or agent provider, and carries a cnf claim naming the key allowed to sign the next hop. Each subsequent hop is a key-binding SD-JWT signed by that key. Intermediate hops use header typ of kb+sd-jwt+kb and carry their own cnf; the final closed mandate uses kb+sd-jwt and must not carry one, which is what makes it terminal. On the wire the hops are joined by a double tilde (~~). The merchant separately signs the checkout payload itself (the checkout_jwt). The result is a credential that records not just an authorization but the whole delegation path that produced it.

  1. 1RootRoot of trust

    The bank or provider signs the root credential.

    root SD-JWT
  2. 2OpenOpen mandate

    The user signs constraints and an allowed key.

    open + cnf
  3. 3ClosedClosed mandate

    The agent signs the exact action, key-bound to the open mandate.

    closed (KB)
  4. 4VerifyVerify

    Trust the root key; each hop checks the previous one.

    follow the chain
Each hop is signed by the key the previous hop authorized, so the closed mandate carries its whole delegation path.

Verification: trust the root, follow the chain

Verification is the mirror image, and it is simple to state. The verifier trusts only the root issuer key. It then validates every hop using the public key bound in the previous hop’s cnf, and checks that the closed mandate’s sd_hash binds the entire preceding chain. If every link holds and the closed mandate satisfies the open mandates above it, the authorization is valid. The Payment Mandate’s binding to the checkout_jwt hash is checked at the same time, so a verifier knows the payment and the cart belong together.

In practice you do not implement SD-JWT by hand. The AP2 SDK provides a MandateClient with create, present, and verify:

# uv pip install git+https://github.com/google-agentic-commerce/AP2.git@main
from ap2.sdk.mandate import MandateClient

client = MandateClient()

# The user (or their provider) issues an open mandate constraining the agent.
open_token = client.create(
    payloads=[open_payment_mandate],   # the v0.2 model shown above
    issuer_key=issuer_jwk,             # EC P-256 root-of-trust key
)

# Later, the agent presents a closed mandate as a new hop in the chain,
# bound to this transaction by nonce and audience.
chain = client.present(holder_key=agent_jwk, mandate_token=open_token,
                       payloads=[closed_payment_mandate],
                       nonce="tx_abc", aud="merchant")

# The merchant or network verifies, trusting only the root issuer key.
payloads = client.verify(
    token=chain,
    key_or_provider=lambda token: issuer_jwk,
    expected_aud="merchant",
    expected_nonce="tx_abc",
)

Two things are worth doing rather than skipping. Pass expected_aud and expected_nonce: without them the final hop’s transaction binding is not enforced. And feed the returned payloads to PaymentMandateChain.parse or CheckoutMandateChain.parse and call verify(...), which is what actually evaluates the constraints; MandateClient.verify checks the cryptography, not the business rules.

Use the v0.2 models under the SDK’s generated package and the canonical JSON schemas, not the older ap2.models shapes, which are the superseded intent-and-cart design.

Receipts: the other half of the record

A mandate proves what was authorized. A receipt proves what the verifier did about it, and v0.2 makes receipts mandatory rather than optional: on accepting or rejecting a mandate, the verifier must return a signed receipt. The Merchant returns a Checkout Receipt, the Merchant Payment Processor returns a Payment Receipt.

A receipt is a verifier-signed JWT carrying a status of Success or Error, the issuer, a timestamp, and a reference that is the hash of the closed mandate it is binding to. Payment Receipts add a payment_id and, on success, psp_confirmation_id and network_confirmation_id; Checkout Receipts add an order_id. In the SDK this is ReceiptClient, and the canonical reference is computed the same way every time, as the SHA-256 of the leaf JWT of the chain:

reference = compute_sha256_b64url(client.get_closed_mandate_jwt(chain))

Receipts are not bookkeeping. They are load-bearing in two places. The specification’s double-spend rule says a shopping agent must not present a further mandate against the same open mandate until it has received a receipt rejecting the previous one, so receipts are the mechanism that stops one open mandate authorizing two carts. And at dispute time the evidence bundle is mandate and receipt on both sides: the checkout mandate with its receipt, the payment mandate with its receipt, each receipt’s reference independently recomputed and matched. AP2 stops there, though. It states plainly that retention, retrieval, and how any of this is used to resolve a dispute are outside its scope.

Human-present versus human-not-present, in the data

This is where the open and closed states pay off. A verifier always receives a closed Checkout Mandate and a closed Payment Mandate, regardless of how the purchase happened. The difference is who signed the closed mandate.

In the human-present (direct) flow, the user sees the final checkout and signs the closed mandates themselves. There is no open mandate in the chain; trust comes directly from the user’s signature.

In the human-not-present (autonomous) flow, the user is not there at the moment of purchase. They earlier signed open mandates that set constraints, and the agent now assembles and signs the closed mandates on their behalf, using the key the open mandate authorized through its cnf. Trust is transitive: the verifier trusts the agent’s closed mandate because it sits in a chain rooted in the user’s signed constraints. The data signal for this flow is the presence of an open mandate (a vct ending in .open.1, carrying a cnf) in the chain. The two are convertible: a merchant can turn an autonomous flow into a present one by returning an unresolved-constraint error that forces the user back into the loop.

How this connects to getting paid

A closed Payment Mandate is the proof a credential provider, network, or processor needs to release funds. In practice the credential provider verifies the Payment Mandate before returning a scoped payment credential, and the specification is strict about the ordering: the credential or token must only be released to the merchant once a final Payment Mandate has been received and verified, which is what binds the token to that one transaction. The merchant payment processor then has to satisfy itself that the credential it received is correctly scoped to the checkout before the money moves.

One naming trap for anyone reading the spec alongside the rest of this site: AP2 uses “MPP” to mean Merchant Payment Processor, one of its five roles. That is not the Machine Payments Protocol, the Stripe and Tempo standard that shares the abbreviation. AP2’s other four roles are the Shopping Agent, the Credential Provider, the Merchant, and the Trusted Surface, the last of which must be non-agentic because it is the surface that captures user consent.

So AP2 produces the authorization, and the payment rails consume it. For the wider money mechanics, see how AI agents pay, and for where AP2 sits among the other standards, see the protocol stack.

FAQ

What is an AP2 mandate, concretely? A signed, tamper-evident credential expressed as an SD-JWT verifiable credential. There are two types, Checkout and Payment, each in two states: an open mandate that constrains future actions and a closed mandate that authorizes one specific action.

How is a mandate signed and verified? With SD-JWT verifiable credentials using EC P-256 keys and ES256 signatures, chained for delegation and joined on the wire by a double tilde. A verifier trusts only the root issuer key; each hop is validated by the previous hop’s cnf key, and the closed mandate’s hash binds the whole chain.

How do the human-present and human-not-present flows differ? The verifier always gets closed Checkout and Payment mandates. In the present flow the user signs the closed mandate; in the not-present flow an agent signs it, with trust supplied by open mandates the user signed earlier.

What is an AP2 receipt? The verifier’s signed answer to a mandate, required on accept or reject: a Checkout Receipt from the merchant, a Payment Receipt from the merchant payment processor. Each carries a Success or Error status and a reference that is the hash of the closed mandate. Receipts are what stop one open mandate authorizing two carts, and the dispute bundle is mandate plus receipt on both sides.

Is AP2 production-ready? It reached v0.2 on 28 April 2026 with an SDK, schemas, and sample scenarios in Python, Go, and Android, but it remains pre-production, with reference implementations rather than documented live real-money deployments. The repo also holds an older model layer that should not be used for new work.

Primary sources

  1. AP2 protocol documentation · AP2 / Google
  2. Agent Payments Protocol (AP2) repository, v0.2 · GitHub, 2026-04-28
  3. AP2 specification v0.2 (roles, modes, mandate versioning, dispute evidence) · AP2
  4. AP2 agent authorization framework (SD-JWT, key binding, verification rules) · AP2
  5. AP2 Checkout Mandate schema, constraints, and receipt · AP2
  6. AP2 Payment Mandate schema, constraints, and receipt · AP2
  7. AP2 Python SDK API reference (MandateClient, ReceiptClient, wire format) · GitHub