TatvaPay
Docs menu· AP2 mandates

Agent Payments Protocol (AP2)

Next

Let 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