Agent Payments Protocol (AP2)
NextLet a shopping agent pay with user-signed AP2 mandates, verified and settled by TatvaPay.
Next (sandbox). AP2 works on the sandbox today, on the fake rail. Nothing is live in production. We implement AP2 v0.2.
AP2 v0.2 secures an agent's purchase with two mandates, each an SD-JWT:
- the Checkout Mandate: what is bought, bound to a checkout the merchant signed;
- the Payment Mandate: how it is paid, bound to the same checkout by its hash.
Each is first open (the user signs limits and names the agent's key in cnf), then closed by the agent for one checkout.
The roles TatvaPay plays
| AP2 role | TatvaPay |
|---|---|
| Shopping agent | not us: your assistant |
| Merchant | we verify checkout mandates for TatvaPay seller agents; the seller signs the checkout |
| Credential provider | the payment instrument is a TatvaPay mandate the person authorised |
| Merchant payment processor | we instruct the licensed partner's UPI rail |
| Trusted surface | the TatvaPay app, where the person authorises the mapped mandate |
1. Register the open mandates
The user's key signs both open mandates (ES256 or EdDSA; its public key in the JWS header as jwk). Constraints TatvaPay understands: checkout.allowed_merchants, checkout.line_items, payment.amount_range (required, INR, paise), payment.allowed_payees, payment.allowed_payment_instruments, payment.allowed_pisps, payment.reference, payment.budget, payment.agent_recurrence, payment.execution_date. Anything else is unresolved_constraint.
POST /ap2/mandates
Authorization: Bearer tpp_<buyer key>
{"open_checkout_mandate": "<sd-jwt>", "open_payment_mandate": "<sd-jwt>"}
TatvaPay maps them to a draft mandate: the amount range's maximum becomes the per-payment cap, the budget (or the range times the recurrence) the total, the payees the allowed sellers, the earliest exp the expiry. The answer has the mandate_id and an approval_url: a person authorises it once in the app, which is where the user's key is bound. It shows in the consent centre with source AP2.
2. The merchant's checkout
The seller agent signs a checkout JWT with its passport key (EdDSA, kid = its key id) and a random jti: merchant, line items (product.price in paise), total, currency: "INR", exp.
3. Present the closed mandates
The agent closes each open mandate with a KB-SD-JWT signed by its cnf key: aud: "tatvapay", a fresh nonce, iat, and sd_hash over the open mandate, joined with ~~.
- closed checkout:
vct: "mandate.checkout.1",checkout_jwt,checkout_hash(base64url SHA-256 of the checkout JWT); - closed payment:
vct: "mandate.payment.1",transaction_id= the checkout hash,payee,payment_amount(paise, INR),payment_instrument: {"type": "tatvapay_mandate", "id": "mdt_…"}.
POST /ap2/payments
Authorization: Bearer tpp_<buyer key>
{"checkout_mandate": "<open>~~<closed>", "payment_mandate": "<open>~~<closed>"}
| Answer | Meaning |
|---|---|
201 settled |
paid; signed checkout_receipt and payment_receipt (JWTs, status: "Success") and the TatvaPay receipt_id |
202 requires_approval |
above ₹2,000: a person approves; GET /ap2/payments/{id} then has the receipts |
4xx |
refused, with signed error receipts: invalid_credential, invalid_mandate, unresolved_constraint, mandate_not_active, replay |
A checkout and a nonce can each be presented once. AP2 receipts are signed with the key published at /.well-known/tatvapay-keys.json; the TatvaPay receipt records ap2_intent_mandate, ap2_cart_mandate and ap2_payment_mandate and verifies offline.
GET /ap2 lists what we support.
A reference shopping agent
examples/ap2-shopping-agent/shopping_agent.py in the engine repository builds every SD-JWT by hand with a crypto library, registers the open mandates, gets a signed checkout from the sample seller, presents the closed mandates and checks both receipts.
Last updated 1 October 2026
Something unclear or wrong? Tell us