AP2 Mandates Explained: Open, Closed, and Payment
Post your first AP2 mandate transactions
Clone the Formance Ledger in GitHub and post each transaction: first the reservation and capture, followed by the release.
Post your first AP2 mandate transactions
Clone the Formance Ledger in GitHub and post each transaction: first the reservation and capture, followed by the release.
Platforms that hold user balances in an omnibus account (a single account holding funds for many underlying owners) need to honor AP2’s mandates: Open Checkout, Closed Checkout, and Payment Mandates. The AP2 protocol specifies the credential formats but says nothing about what your core ledger records when a mandate arrives.
An agent presents an Open Checkout Mandate with intent_expiry three weeks out, alongside a Payment Mandate constraints JSON Web Token (JWT) carrying payment.amount_range. The user has signed, but no cart exists yet, so your finance lead asks what the balance shows, and nobody on the team can answer.
Each mandate does a different job in this failed scenario. Open Checkout Mandates authorize future spending within signed constraints, while Closed Checkout Mandates bind a specific cart at a specific price. Payment Mandates give the network and issuer cryptographic proof of the agentic payment.
Each of those maps to a ledger action. Your core ledger reserves funds on the open mandate, captures on the closed mandate, and releases the remainder on expiry or a rejection receipt.
AP2 v0.2 collapsed the original three-mandate model into two mandate types: the Checkout Mandate and the Payment Mandate, each with "open" and "closed" variants.
The Intent Mandate no longer exists as a separate type because the open mandates now handle its role in human-not-present flows, and verifiers always receive closed mandates regardless of mode.
| v0.1 mandate | v0.2 equivalent | v0.2 vct claim (closed / open) |
| Cart Mandate | Closed Checkout Mandate | mandate.checkout.1 / mandate.checkout.open.1 |
| Intent Mandate | Folded into open mandates | mandate.checkout.open.1 / mandate.payment.open.1 |
| Payment Mandate | Payment Mandate (open + closed) | mandate.payment.1 / mandate.payment.open.1 |
On the day v0.2 shipped, Google donated AP2 to the FIDO Alliance, and the v0.2.0 tag introduced Human-Not-Present payments, samples, and a Python software development kit (SDK).
An Open Checkout Mandate is a credential the Shopping Agent drafts, and the user approves on a Trusted Surface. It lets the agent complete a checkout later, but only within the constraints the user approved. It is signed with the user's key, or with the Agent Provider's key when the Agent Provider is the party verifiers trust.
v0.2 secures every mandate as a Selective Disclosure JSON Web Token (SD-JWT) and adds Human-Not-Present payments and replay defenses.
An open mandate isn't yet bound to a specific transaction. It carries constraints that any closed mandate built from it must meet.
In the Human-Not-Present flow, the user approves the agent's proposed constraints rather than a final cart. The agent sends both the open and closed Checkout Mandates to the merchant.
If the merchant or Credential Provider can't confirm the constraints are met, it can return an unresolved_constraint error and bring the user back to approve the closed mandates directly.
AP2 hash-links each closed mandate to its open one, but it doesn't say where those records live. AP2 doesn't handle retention or retrieval of dispute evidence.
The mandates and receipts can sit with up to five parties: the Shopping Agent, Merchant, Credential Provider, Network, and Merchant Payment Processor. The ledger holding the balance should be the system of record for that evidence. Formance is an open-source, programmable core ledger with regulatory-grade transaction traceability, built for that role.
When the reservation lives as a posting in the core ledger, "how much is left on mandate m_7f3a" is a balance query, and the record of how AI agents move money inside your omnibus account is the same that your finance team queries.
An Open Checkout Mandate limits what the agent can buy by constraining the user-approved options. AP2 v0.2 defines two:
The open mandate also carries the agent's public key in a cnf claim. Set its exp timestamp to the shortest window that lets the agent finish the task.
Without a signed mandate, an agent acting on stored payment credentials can turn a vague search request into purchase authority, with no cryptographic evidence the user approved the spend. Under AP2, a closed mandate signed by the agent is only valid if it meets every constraint in the open mandate the user approved.
An agent may close an open mandate with its own Agent Key. The agent must present both the open and closed mandates, and it can't sign another closed mandate against the same open mandate until the previous one comes back rejected.
The Shopping Agent "MAY now sign it using its Agent Key instead of getting approval on a Trusted Surface. It then MUST provide both the user-signed open Mandate and the agent-signed closed Mandate to the Verifying Parties."
On sequencing, the specification is even stricter, stating that "Shopping Agents MUST NOT present any subsequent open Payment or Checkout Mandates without receiving a rejection receipt from the previous one. This is to prevent an Agent approving multiple different Checkouts using the same open Mandate.”
For your ledger, the rejection receipt triggers the release, and your platform verifies the receipt before returning any reservation.
A Closed Checkout Mandate authorizes one specific, finalized checkout. The merchant signs the checkout details as a JWT. The Shopping Agent wraps that JWT in the mandate along with a checkout_hash, which binds the mandate to that exact cart.
Who signs the mandate depends on the flow. In Human-Present, the Trusted Surface signs it after the user approves the cart. In Human-Not-Present, the agent signs it with its own key, and its authority comes from the open mandate the user approved.
Treat the capture transaction as the point of transaction finality. The signature says the user agreed to pay, while the capture transaction says funds left the reserved balance. Network authorization sits between the two, and it can decline or time out.
In the Human-Present flow, the user sees the final checkout and approves it directly. The Shopping Agent builds the Checkout and Payment Mandate contents from the merchant's signed Checkout JWT and sends them to a Trusted Surface.
The Trusted Surface shows them to the user and signs them once the user approves. The Trusted Surface must run on deterministic code, so no LLM sits between the user and the signature.
The Payment Mandate goes to the Credential Provider, which verifies it and returns a payment credential. The payment credential and the Checkout Mandate go to the Merchant, which checks the checkout against the one it created.
For merchant-initiated charges, the Merchant then starts the payment with its processor. The Merchant Payment Processor confirms the payment credential is scoped to that checkout before processing it.
The signature covers the merchant-signed checkout, including every item and its stated price. Any change the merchant makes afterward, like a reprice or a substituted SKU, changes the checkout_jwt, breaks the checkout_hash, and requires a new closed Checkout Mandate. Your platform should require every capture to match the signed checkout exactly.
That exact-match rule is what keeps the closed mandate useful as dispute evidence months later. If the merchant swaps a line item or nudges the total between signature and capture, the signed cart no longer describes what the buyer actually paid for. The chain from user consent to funds movement breaks.
A capture that references a modified cart should be rejected at the ledger boundary, not reconciled after the fact.
A Payment Mandate is the SD-JWT that gives the Credential Provider, Network, and Merchant Payment Processor cryptographic proof that the Shopping Agent is authorized to pay for a specific Checkout, and it lets a verifier walk the chain from the user's signed authorization to the closed mandate.
In the Human-Present flow, the Payment Mandate is a single closed mandate. In the Human-Not-Present flow, it's presented as a chain of two SD-JWTs.
The first is the open Payment Mandate, which holds the user-approved constraints and the agent's key. The second is the closed Payment Mandate, a key-bound JWT the agent signs.
Presenting them produces a receipt. The AP2 specification describes a Checkout Receipt for an accepted or rejected Checkout Mandate and a signed Payment Receipt for an accepted or rejected Payment Mandate.
Because Checkout and Payment Mandates serve as evidence at the time of dispute, your platform should store the AP2 payment mandate SD-JWT and its receipt where a dispute team can retrieve them.
The open Payment Mandate holds the payment constraints the user approved. AP2's reference example uses three:
AP2 also defines constraints for allowed payment instruments, allowed PISPs, execution date, agent recurrence, and total budget. The open mandate uses vct: mandate.payment.open.1 and carries the agent's key in cnf, plus iat and exp timestamps.
The closed Payment Mandate commits to the actual payment. It uses vct: mandate.payment.1 and carries:
Its key-binding JWT adds aud, nonce, and sd_hash. The sd_hash binds the closed mandate to the open one it was issued against.
Every open mandate must carry a cnf claim naming the agent's public key.
In AP2's examples, that's an EC key on the P-256 curve, and mandates are signed with ES256. The agent closes the mandate by signing a key-binding JWT with the matching private key.
A verifier checks the signature on the open mandate, then checks that the closed mandate was signed with the key in cnf.
A core ledger honors AP2 mandates through three double-entry postings inside your omnibus account. These include a reserve at open-mandate issuance, a capture at closed-mandate execution, and a release at exp or on a rejection receipt.
Each lifecycle event is an independently submitted double-entry accounting transaction under the ledger posting model. The buyer's available and reserved accounts sit under the same omnibus balance, so a reservation moves value between the buyer's own accounts and never touches the bank.
Duplicate-spend prevention falls to your integration, and AP2 provides no mechanism for it. Use the Payment Mandate's transaction_id as capture-transaction metadata and reject any reused transaction_id before submitting another capture.
Numscript, Formance's transaction language, describes each posting as a send from a source account to a destination account, with metadata attached.
Every transaction carries mandate_id, and the capture also carries transaction_id, so reconciliation against payment processor settlement can read metadata straight from the capture instead of joining external logs.
Move [USD/2 25000000] from @buyers:acme:available to @buyers:acme:mandate:7f3a:reserved.
The posting is $250,000.00 and carries mandate_id and exp as transaction metadata. When the buyer accepts the open mandate, the buyer's spendable balance drops by the full authorized ceiling, preventing a second agent from committing the same funds.
// OPEN_MANDATE_RESERVE
// Event: reserve the open mandate's authorized ceiling against the buyer's balance
send [USD/2 25000000] (
source = @buyers:acme:available
destination = @buyers:acme:mandate:7f3a:reserved
)
set_tx_meta("event_type", "open_mandate_reserve")
set_tx_meta("mandate_id", "m_7f3a")
set_tx_meta("exp", "2026-11-14T00:00:00Z")
Move [USD/2 18750000] from @buyers:acme:mandate:7f3a:reserved to @merchants:412:payable.
The posting matches the closed mandate's payment_amount of USD 187,500.00 and carries the transaction_id and the closed Checkout Mandate's vct as transaction metadata.
// CLOSED_MANDATE_CAPTURE
// Event: capture the closed mandate's payment amount to the merchant payable
send [USD/2 18750000] (
source = @buyers:acme:mandate:7f3a:reserved
destination = @merchants:412:payable
)
set_tx_meta("event_type", "closed_mandate_capture")
set_tx_meta("mandate_id", "m_7f3a")
set_tx_meta("transaction_id", "txn_9c21")
set_tx_meta("vct", "mandate.checkout.1")
Return the remaining [USD/2 6250000] from the reserved account to @buyers:acme:available, effective-dated to exp or to the rejection receipt's timestamp by setting the transaction's timestamp on the request.
The release records the authorization's end time regardless of the scheduler's execution time.
// OPEN_MANDATE_RELEASE
// Event: release the open mandate's remaining reservation on expiry or rejection
send [USD/2 6250000] (
source = @buyers:acme:mandate:7f3a:reserved
destination = @buyers:acme:available
)
set_tx_meta("event_type", "open_mandate_release")
set_tx_meta("mandate_id", "m_7f3a")
set_tx_meta("release_reason", "intent_expiry")
The three postings give you a complete audit trail for every AP2 mandate: the reserve locks the authorization ceiling, the capture moves only what the closed mandate actually commits, and the release returns whatever was not spent.
Each posting carries the mandate_id (and the capture carries transaction_id). Therefore, the ledger can answer questions like "how much of this mandate is still available," "which capture drew against it," and "when did the authorization end," without joining external logs.
Your ledger is ready for AP2 when it passes six tests: one reservation per open mandate, one capture per checkout, and every transaction traced to the mandate that authorized it. Run them before the first agent presents a mandate:
The ledger now holds the mandate state and the evidence behind it. Your integration still verifies every mandate and rejects duplicate presentations before posting a capture.