diff --git a/docs/specification/checkout-rest.md b/docs/specification/checkout-rest.md index f1c34134e..52172d8ca 100644 --- a/docs/specification/checkout-rest.md +++ b/docs/specification/checkout-rest.md @@ -97,7 +97,7 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version {"type": "shop_pay"} ], "config": { - "merchant_id": "shop_merchant_123" + "key_id": "rzp_live_ABCD" } } ] @@ -245,7 +245,7 @@ so clients must include all previously set fields they wish to retain. {"type": "shop_pay"} ], "config": { - "merchant_id": "shop_merchant_123" + "key_id": "rzp_live_ABCD" } } ] @@ -600,7 +600,7 @@ Follow-up calls after initial `fulfillment` data to update selection. {"type": "shop_pay"} ], "config": { - "merchant_id": "shop_merchant_123" + "key_id": "rzp_live_ABCD" } } ] @@ -948,7 +948,7 @@ place to set these expectations via `messages`. {"type": "shop_pay"} ], "config": { - "merchant_id": "shop_merchant_123" + "key_id": "rzp_live_ABCD" } } ] diff --git a/docs/specification/examples/encrypted-credential-handler.md b/docs/specification/examples/encrypted-credential-handler.md index 97b175fb5..f7da28d72 100644 --- a/docs/specification/examples/encrypted-credential-handler.md +++ b/docs/specification/examples/encrypted-credential-handler.md @@ -117,7 +117,7 @@ compliant** because they will handle raw PANs. This includes: ### Handler Configuration -Businesses advertise the platform's handler. The `business_id` field identifies +Businesses advertise the platform's handler. The `key_id` field identifies the business, which the platform uses to look up the correct public key for encryption. @@ -136,7 +136,7 @@ have their own compliance requirements. | Field | Type | Required | Description | | :-------------- | :----- | :------- | :-------------------------------------------------- | | `environment` | string | Yes | API environment (`sandbox` or `production`) | -| `business_id` | string | Yes | Business identifier assigned by platform | +| `key_id` | string | Yes | Business identifier assigned by platform | | `public_key_id` | string | Yes | Identifier for the business's registered public key | #### Example Business Handler Declaration @@ -162,7 +162,7 @@ have their own compliance requirements. ], "config": { "environment": "production", - "business_id": "merchant_abc123", + "key_id": "merchant_abc123", "public_key_id": "key_2026_01" } } @@ -179,7 +179,7 @@ The response config includes information about the encryption used. | Field | Type | Required | Description | | :--------------------- | :----- | :------- | :------------------------------------ | | `environment` | string | Yes | API environment | -| `business_id` | string | Yes | Business identifier | +| `key_id` | string | Yes | Business identifier | | `encryption_algorithm` | string | Yes | Algorithm used (e.g., `RSA-OAEP-256`) | | `key_id` | string | Yes | Key identifier used for encryption | @@ -199,7 +199,7 @@ The response config includes information about the encryption used. ], "config": { "environment": "production", - "business_id": "merchant_abc123", + "key_id": "merchant_abc123", "encryption_algorithm": "RSA-OAEP-256", "key_id": "key_2026_01" } diff --git a/docs/specification/examples/platform-tokenizer-payment-handler.md b/docs/specification/examples/platform-tokenizer-payment-handler.md index 62bbc41e6..ad339367b 100644 --- a/docs/specification/examples/platform-tokenizer-payment-handler.md +++ b/docs/specification/examples/platform-tokenizer-payment-handler.md @@ -188,7 +188,7 @@ credential type (e.g., PCI DSS for cards). | Field | Type | Required | Description | | :------------ | :----- | :------- | :------------------------------------------ | | `environment` | string | Yes | API environment (`sandbox` or `production`) | -| `business_id` | string | Yes | Business identifier assigned by platform | +| `key_id` | string | Yes | Business identifier assigned by platform | #### Example Business Handler Declaration @@ -213,7 +213,7 @@ credential type (e.g., PCI DSS for cards). ], "config": { "environment": "production", - "business_id": "business_abc123" + "key_id": "business_abc123" } } ] @@ -229,7 +229,7 @@ The response config includes runtime token lifecycle information. | Field | Type | Required | Description | | :------------------ | :------ | :------- | :---------------------------- | | `environment` | string | Yes | API environment | -| `business_id` | string | Yes | Business identifier | +| `key_id` | string | Yes | Business identifier | | `token_ttl_seconds` | integer | No | Token time-to-live in seconds | #### Example Response Config @@ -248,7 +248,7 @@ The response config includes runtime token lifecycle information. ], "config": { "environment": "production", - "business_id": "business_abc123", + "key_id": "business_abc123", "token_ttl_seconds": 900 } } diff --git a/docs/specification/examples/processor-tokenizer-payment-handler.md b/docs/specification/examples/processor-tokenizer-payment-handler.md index d48063909..444353f2c 100644 --- a/docs/specification/examples/processor-tokenizer-payment-handler.md +++ b/docs/specification/examples/processor-tokenizer-payment-handler.md @@ -103,7 +103,7 @@ The handler's specification (referenced via the `spec` field) documents the | Field | Type | Required | Description | | :------------ | :----- | :------- | :------------------------------------------ | | `environment` | string | Yes | API environment (`sandbox` or `production`) | -| `business_id` | string | Yes | Business identifier with the processor | +| `key_id` | string | Yes | Business identifier with the processor | #### Example Business Handler Declaration @@ -128,7 +128,7 @@ The handler's specification (referenced via the `spec` field) documents the ], "config": { "environment": "production", - "business_id": "merchant_xyz789" + "key_id": "merchant_xyz789" } } ] @@ -144,7 +144,7 @@ The response config includes runtime information about what's available for this | Field | Type | Required | Description | | :------------ | :----- | :------- | :------------------------------------- | | `environment` | string | Yes | API environment used for this checkout | -| `business_id` | string | Yes | Business identifier | +| `key_id` | string | Yes | Business identifier | #### Example Response Config @@ -162,7 +162,7 @@ The response config includes runtime information about what's available for this ], "config": { "environment": "production", - "business_id": "merchant_xyz789" + "key_id": "merchant_xyz789" } } ``` @@ -206,7 +206,7 @@ business's configuration. ], "config": { "environment": "production", - "business_id": "merchant_xyz789" + "key_id": "merchant_xyz789" } } ] diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 44c0ce59d..3f2c998b5 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1220,7 +1220,7 @@ an encrypted payment token. "environment": "TEST", "merchant_info": { "merchant_name": "Example Merchant", - "merchant_id": "01234567890123456789", + "key_id": "01234567890123456789", "merchant_origin": "checkout.merchant.com" }, "allowed_payment_methods": [ diff --git a/docs/specification/payment-handler-guide.md b/docs/specification/payment-handler-guide.md index 46cafc7df..b5e1f09a4 100644 --- a/docs/specification/payment-handler-guide.md +++ b/docs/specification/payment-handler-guide.md @@ -88,7 +88,7 @@ platform), but **MAY** define additional participants with specific roles. **Note on Terminology:**: While this guide refers to the participant as the **"Business"**, technical schema fields may retain the standard industry -nomenclature **`merchant_*`** (e.g., `merchant_id`, `merchant_name`). +nomenclature **`key_*`** (e.g., `key_id`, `merchant_name`). Specifications **MUST** explicitly document these field mappings. **Standard Participants:** @@ -142,7 +142,7 @@ document: - Prerequisites typically occur out-of-band (portals, contracts, API calls) - Multiple participants **MAY** have independent prerequisites - The identity from prerequisites typically appears within the handler's - `config` object (e.g., as `merchant_id` or similar handler-specific field) + `config` object (e.g., as `key_id` or similar handler-specific field) - Participants receiving raw credentials (e.g., businesses, PSPs) typically must complete security acknowledgements during onboarding, accepting responsibility for credential handling and compliance ### Handler Declaration @@ -229,7 +229,7 @@ and typically includes different configuration: ], "config": { "environment": "production", - "business_id": "business_xyz_789" + "key_id": "rzp_live_ABCD" } } ``` @@ -274,7 +274,7 @@ and typically includes different configuration: "config": { "api_version": 2, "environment": "production", - "business_id": "business_xyz_789" + "key_id": "rzp_live_ABCD" } } ``` @@ -428,7 +428,7 @@ Each variant has its own config schema tailored to its context: "enum": ["sandbox", "production"], "default": "production" }, - "business_id": { + "key_id": { "type": "string", "description": "Business identifier for this handler." } @@ -472,7 +472,7 @@ Each variant has its own config schema tailored to its context: "type": "string", "enum": ["sandbox", "production"] }, - "business_id": { + "key_id": { "type": "string", "description": "Business identifier for this handler." }, diff --git a/docs/specification/payment-handler-template.md b/docs/specification/payment-handler-template.md index baed124c0..bdd5f30f3 100644 --- a/docs/specification/payment-handler-template.md +++ b/docs/specification/payment-handler-template.md @@ -46,7 +46,7 @@ supports.} > **Note on Terminology:** > While this specification refers to the participant as the **"business,"** > technical schema fields may retain the standard industry nomenclature -> **`merchant_*`** (e.g., `merchant_id`). Mappings are documented below. +> **`key_*`** (e.g., `key_id`). Mappings are documented below. | Participant | Role | Prerequisites | | :---------------------- | :----------------- | :--------------------------- | @@ -97,7 +97,7 @@ Before advertising this handler, businesses **MUST** complete: | Field | Description | | :---------------------- | :----------------------------------------------- | -| `identity.access_token` | {what identifier is assigned, e.g., business_id} | +| `identity.access_token` | {what identifier is assigned, e.g., key_id} | | {additional config} | {any additional configuration from onboarding} | ### Handler Configuration diff --git a/docs/specification/playground.md b/docs/specification/playground.md index 4ff27bbf9..2905da687 100644 --- a/docs/specification/playground.md +++ b/docs/specification/playground.md @@ -438,7 +438,7 @@ const UcpData = { environment: "TEST", merchant_info: { merchant_name: "Example Merchant", - merchant_id: "01234567890123456789", + key_id: "01234567890123456789", merchant_origin: "checkout.merchant.com", auth_jwt: "edxsdfoaisjdfapsodjf...." }, diff --git a/docs/specification/razorpay-upi-circle-payment-handler.md b/docs/specification/razorpay-upi-circle-payment-handler.md new file mode 100644 index 000000000..b16220c2a --- /dev/null +++ b/docs/specification/razorpay-upi-circle-payment-handler.md @@ -0,0 +1,789 @@ + + +# Razorpay UPI Circle Payment Handler + +* **Handler Name:** `com.razorpay.upi_circle` +* **Version:** `{{ ucp_version }}` + +## Introduction + +The `com.razorpay.upi_circle` handler enables businesses to accept **delegated +UPI payments** through UCP-compatible platforms. UPI Circle is NPCI's +delegation framework — buyers authorize an AI agent to make payments on their +behalf within configurable limits, without per-transaction MPIN authentication. + +This handler follows the standard credential-before-Complete-Checkout pattern — the same architecture as com.google.pay and dev.shopify.shop_pay: the platform fetches a one-time cryptogram from Razorpay AI Commerce TSP after authenticating the user, sends it in Complete Checkout and the business processes the payment by passing the cryptogram to its PSP(Razorpay) and marks the Checkout as complete. No escalation, no app switch, no buyer interaction at payment time. + +> **Note:** This handler requires a **one-time delegation setup** between the +> buyer, the Platform and Razorpay. This setup happens outside UCP as a +> bilateral integration between the platform and Razorpay AI Commerce TSP +> acting like a Credential Provider, similar to a buyer saving a card. +> See [Delegation Setup](#delegation-setup-outside-ucp). + +### Key Benefits + +* **Truly agentic** — No per-transaction authentication. Agent pays autonomously within mandate limits. +* **Zero PCI-DSS scope** — VPA-based; no card data touches any system. +* **Zero buyer friction** — No app switch, no QR code, no MPIN at payment time. Like card-on-file for UPI. +* **NPCI-backed guardrails** — Per-transaction and monthly limits enforced at the mandate level by NPCI and the buyer's bank. +* **500M+ UPI users** — Extends India's dominant payment rail to agentic commerce. + +### Integration Guide + +| Participant | Section | +| :------------------------------- | :-------------------------------------------------------- | +| **Business** | [Business Integration](#business-integration) | +| **Platform** | [Platform Integration](#platform-integration) | +| **PSP** | [PSP Integration](#psp-integration-razorpay) | +| **AI Commerce TSP** | [Credential Provider](#razorpay-ai-commerce-tsp-as-a-credential-provider-delegation-setup-and-cryptogram-issuance) | + +--- + +## Participants + +> **Note on Terminology:** This specification refers to the Razorpay merchant +> account holder as the **"business."** Technical schema fields retain standard +> industry nomenclature `key_*` where applicable. + +| Participant | Role | Prerequisites | +| :-------------------------- | :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------ | +| **Business** | Advertises handler configuration; processes cryptogram via Razorpay; responds `completed` | Yes — Razorpay account with KYC, webhook endpoint (HTTPS, TLS 1.2+) | +| **Platform** | Discovers handler; enables product discovery; takes UPI delegation from a user through a AI Commerce TSP; fetches one time cryptogram as payment credentials and submits in Complete Checkout | Yes - Requires custom integration with Razorpay AI Commerce TSP for setting up the delegation and fetching one time cryptogram, active `delegate_id` | +| **Razorpay (PSP)** | Processes delegated debit via NPCI using mandate UMN; fires webhook to business | N/A — payment infrastructure | +| **Razorpay AI Commerce TSP (Credential Provider)** | Allows delegation to the platform and issues one-time cryptograms for established delegation mandates. | N/A — credential infrastructure (analogous to Google Pay token API) | + +--- + +## Delegation Setup (Outside UCP) + +Before any payment can occur, the buyer must establish a delegation mandate. +This is a **one-time bilateral integration** between the Platform and Razorpay +— it is **not part of UCP**, just as saving a card on Gemini is not part of +the checkout protocol. + +### Setup Flow + +``` ++----------+ +---------------------+ +---------------------+ +---------+ +| User | | AI Agent (Platform)| | RZP AI Commerce TSP | | UPI App | ++----+-----+ +----------+----------+ +----------+----------+ +----+----+ + | | | | + | 1. Initiates UPI set-up | | | + | with mobile number | | | + |------------------------>| | | + | | 2. Initiate customer mobile | | + | | number verification | | + | |----------------------------->| | + | | | | + | | | | + | | | | + | 3. One Time Password | | | + | (OTP sent to User) - - - - - - - - - - - - - - - - -| | + |<-------------------------------------------------------| | + | | | | + | 4. Customer provides | | | + | OTP on Platform | | | + |------------------------>| | | + | | 4. Submit OTP to Razorpay | | + | |----------------------------->| | + | | 5. Return Delegation Intent | | + | | URL and/or QR code | | + | |<-----------------------------| | + | | | | + | 6. Show Intent URL | | | + |<------------------------| | | + | | | | + | 7. Goes to UPI app, sets up limits & confirms | | + | payment delegation to Platform | | + |------------------------------------------------------->|----------------------->| + | | | | + | | 8. User redirected back | | + | | to Platform | | + | |<------------------------------------------------------| + | | | | + | | 9. Check Delegation status | | + | |----------------------------->| | + | | 10. Confirmation of | | + | | delegation status | | + | | with delegate_ID | | + | |<-----------------------------| | + | | | | +``` + +### Setup API Reference + +| Operation | Method | Purpose | +| :--------------------------------- | :----- | :-------------------------------------------------- | +| Generate OTP | POST | Send OTP to buyer's mobile | +| Submit OTP | POST | Verify OTP; receive intent link / QR for delegation | +| Poll delegation status | GET | Poll until `delegate_id` reaches `status: linked` | + +**Output:** `delegate_id` — persistent mandate reference. All subsequent +payments use this ID to fetch cryptograms. + +> Get in touch with Razorpay for API Docs + + +--- + +## End-to-End Payment Flow + +### Flow Diagram + +``` ++------------+ +---------------------------+ +------------------+ +------------------+ +| Platform | | Razorpay (Cred. Provider)| | Business | | Razorpay (PSP) | ++-----+------+ +-------------+-------------+ +--------+---------+ +--------+---------+ + | | | | + ═════════════════════════════════╪════════════════════════════╪══════════════════════╪════ + PRE-CONDITION: Delegation setup completed . + Platform holds a valid delegate_id (status: linked). + ════╪════════════════════════════╪════════════════════════════╪══════════════════════╪════ + | | | | + | 1. GET /.well-known/ucp | | | + |--------------------------->| (or business profile) | | + | 2. Handler config | | | + |<---------------------------| | | + | | | | + | 3. POST /checkout | | | + | (create) | | | + |-------------------------------------------------------->| | + | 4. Checkout response | | | + | (upi_circle available) | | | + |<--------------------------------------------------------| | + | | | | + [Platform selects upi_circle for buyer with active delegate_id] | + | | | | + | 5. GET payment cryptogram | | | + | for the saved | | | + | delegation id. | | | + | | | | + |--------------------------->| | | + | 6. { cryptogram, | | | + | expires_at } | | | + |<---------------------------| | | + | | | | + [Platform builds instrument + credential, submits Complete Checkout] | + | | | | + | 7. POST checkout/complete | | | + | instrument: | | | + | type: upi_circle | | | + | delegate_id: c894aa29 | | | + | credential: | | | + | type: upi_circle_ | | | + | cryptogram | | | + | cryptogram: a345345... | | | + | expires_at: ... | | | + |-------------------------------------------------------->| | + | | | | + | | | 8.Validate handler | + | | | Validate idempot. | + | | | Extract cryptogram| + | | | | + | | | 9. Process payments | + | | | through PSP | + | | (delegate_id + cryptogram) + | | |--------------------->| + | | | | + | | | 10. Webhook: | + | | | payment.authorized | + | | |<---------------------| + | | | | + | | | 11. Verify signature | + | | | Update checkout | + | | | completed | + | | | | + | 12. status: completed | | | + |<--------------------------------------------------------| | + | (or complete_in_ | | | + | progress → poll) | | | +``` + +--- + +## Business Integration + +### Prerequisites + +Before advertising this handler, businesses **MUST** complete: + +1. **Create a Razorpay account** at [dashboard.razorpay.com](https://dashboard.razorpay.com). +2. **Complete KYC** to activate live payments. +3. **Configure webhook endpoint** — HTTPS endpoint (TLS 1.2+) to receive + `payment.authorized` and `payment.failed` events. +4. **Retrieve credentials** from *Settings → API Keys*: + * `key_id` / `key_secret` — for webhook signature verification and PSP API calls. + +> **Note:** No merchant VPA is needed in the handler config. The payee VPA is +> resolved by Razorpay at payment time from the `delegate_id` and cryptogram. + +### Handler Configuration + +#### Handler Schema + +| Config Variant | Context | Key Fields | +| :---------------- | :------------------- | :-------------------------------- | +| `business_config` | Business discovery | `environment`, `key_id` | +| `platform_config` | Platform discovery | `environment`, `upi_apps` | +| `response_config` | Checkout responses | `environment` | + +#### Business Config Fields + +| Field | Type | Required | Description | +| :-------------- | :----- | :------- | :----------------------------------------------------------------------------------- | +| `key_id` | string | Yes | Razorpay public API key (`rzp_test_*` or `rzp_live_*`) | +| `environment` | string | Yes | `sandbox` or `production` | +| `merchant_name` | string | No | Business display name shown to the buyer during the UPI payment flow | +| `currency` | string | No | Currency accepted. Defaults to `INR` | + +#### Platform Config Fields + +| Field | Type | Required | Description | +| :---------- | :-------------- | :------- | :------------------------------------------------------------------------------------------------------- | +| `environment` | string | Yes | `sandbox` or `production`. Must match the business config environment. | +| `upi_apps` | array of strings | No | UPI apps the platform is capable of deep-linking to (e.g., `["gpay", "phonepe", "paytm", "bhim"]`). Each value must be unique. | + +#### Example Handler Declaration + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "payment_handlers": { + "com.razorpay.upi_circle": [ + { + "id": "razorpay_upi_circle", + "name": "com.razorpay.upi_circle", + "config": { + "key_id": "rzp_live_KN2V9UHAlkas", + "environment": "production" + } + } + ] + } + } +} +``` + +### Processing Payments + +Upon receiving a Complete Checkout with a `upi_circle` instrument and +`upi_circle_cryptogram` credential, businesses **MUST**: + +1. **Validate handler.** Confirm `instrument.handler_id` matches a declared + `com.razorpay.upi_circle` handler. + +2. **Ensure idempotency.** If this `checkout_id` has already been processed, + return the previous result without re-processing. + +3. **Extract credential.** Retrieve `credential.cryptogram` and + `credential.delegate_id` from the instrument. + +4. **Process delegated debit.** Call Razorpay PSP to initiate a delegated + debit via NPCI using the mandate's UMN (resolved internally by Razorpay from + `delegate_id`, `cryptogram`). +5. **Receive Razorpay Webhook** On the configured URL + +6. **Return result.** + +**Success Response:** + +```json +{ + "id": "checkout_abc123", + "status": "completed", + "order": { + "id": "order_xyz789", + "permalink_url": "https://store.com/orders/xyz789" + } +} +``` + +**Failure Response:** + +```json +{ + "id": "checkout_abc123", + "status": "canceled", + "messages": [ + { + "type": "error", + "code": "payment_declined", + "severity": "recoverable", + "content": "Delegated UPI payment was declined. The mandate limit may have been exceeded.", + "path": "$.payment" + } + ] +} +``` + +### Webhook Handling + +Razorpay sends payment events to the business's configured webhook endpoint. +Businesses **MUST** verify signatures before processing. + +```python +import hmac +import hashlib + +def verify_razorpay_webhook(body: bytes, signature: str, webhook_secret: str) -> bool: + expected = hmac.new( + webhook_secret.encode("utf-8"), + body, + hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(expected, signature) +``` + +| Event | Meaning | Business Action | +| :--------------------- | :----------------------------------- | :------------------------------------------------------ | +| `payment.authorized` | NPCI confirmed delegated debit | Update checkout to `completed`, populate `order` | +| `payment.failed` | Debit declined (limit, expiry, etc.) | Update checkout to `canceled` with error messages | + +--- + +## Platform Integration + +### Prerequisites + +Before handling `com.razorpay.upi_circle` payments, platforms **MUST**: + +1. **Integrate with Razorpay AI Commerce TSP** — Razorpay AI Commerce TSP acting as + a Credential Provider. Platform must authenticate the user to store delegation + against the unique delegate_id, and fetch one time cryptogram. +2. **Complete delegation setup** — Buyer must have an active delegation mandate + setup via Razorpay AI Commerce TSP. Platform must hold a valid delegate_id. +3. **Instrument Selection** - Ability to select UPI Circle as one of the payment + instrument during checkout. +3. **Implement status polling** — Poll `GET /checkout-sessions/{id}` if the + business processes asynchronously and returns `status: complete_in_progress`. + +Platforms **SHOULD** only present UPI Circle as a payment instrument when: +- The buyer's context indicates India (e.g., `address_country: "IN"`) +- The buyer has an active delegation (`delegate_id` with `status: linked`) + - Also allow the user to setup a delegation of not already done + +### Platform Handler Integration + +The following is a sample platform profile declaring support for +`com.razorpay.upi_circle`: + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "payment_handlers": { + "com.razorpay.upi_circle": [ + { + "id": "platform_razorpay_upi_circle", + "version": "{{ ucp_version }}", + "spec": "https://razorpay.com/ucp/handlers/upi-circle", + "schema": "https://razorpay.com/ucp/handlers/upi-circle/schema.json", + "available_instruments": [ + { "type": "upi_circle" } + ], + "config": { + "environment": "production", + "upi_apps": ["gpay", "phonepe", "paytm", "bhim"] + } + } + ] + } + } +} +``` + +| Field | Required | Description | +| :---- | :------- | :---------- | +| `id` | Yes | Unique platform-scoped identifier for this handler entry | +| `version` | Yes | UCP spec version the platform targets | +| `spec` | Yes | Canonical handler spec URL | +| `schema` | Yes | JSON schema URL for validation | +| `available_instruments[].type` | Yes | Must be `"upi_circle"` | +| `config.environment` | Yes | `"sandbox"` for test mode, `"production"` for live | +| `config.upi_apps` | No | UPI apps the platform can deep-link to for delegation setup | + +> Use `"environment": "sandbox"` during development and integration testing. +> Switch to `"production"` only after completing Razorpay's go-live checklist. + +### Payment Protocol + +#### Step 1: Discover Handler + +Identify `com.razorpay.upi_circle` in the business's `payment.handlers` array +from the Create Checkout response. + +```json +{ + "id": "razorpay_upi_circle", + "name": "com.razorpay.upi_circle", + "config": { + "key_id": "rzp_live_KN2V9UHAlkas", + "environment": "production" + } +} +``` + +#### Step 2: Fetch Cryptogram from Razorpay AI Commerce TSP + +Call Razorpay's cryptogram issuance API to get a one-time cryptogram for the +buyer's delegation. This **MUST** be done immediately before submitting +Complete Checkout — cryptograms are single-use and time-limited. + +```http +POST /v1/upi/delegate/{delegate_id}/cryptogram +Authorization: Basic base64(key_id:key_secret) +Content-Type: application/json + +{ + "delegate_id": "c894aa29a7da69" +} +``` + +> Exact API path subject to change. Refer to Razorpay developer documentation +> for the current endpoint. + +**Response:** + +```json +{ + "delegate_id": "c894aa29a7da69", + "cryptogram_value": "a345345dfgdfasdfh45jtyhgjkyutsdasd2", + "expired_at": 1748716199 +} +``` + +> Cryptograms are **single-use** — NPCI rejects reuse. Fetch a fresh cryptogram +> for each transaction. Do not cache or reuse across retries. + +#### Step 3: Build Instrument + Credential (Minting Credential) + +```json +{ + "id": "pi_001", + "handler_id": "razorpay_upi_circle", + "type": "upi_circle", + "delegate_id": "c894aa29a7da69", + "selected": true, + "display": { + "name": "UPI Autopay", + "masked_vpa": "roh***@icici" + }, + "credential": { + "type": "upi_circle_cryptogram", + "cryptogram": "a345345dfgdfasdfh45jtyhgjkyutsdasd2", + "delegate_id": "c894aa29a7da69", + "expires_at": "2026-03-26T12:15:00Z" + } +} +``` + +#### Step 4: Submit Complete Checkout + +```http +POST /checkout-sessions/{checkout_id}/complete +Content-Type: application/json +UCP-Agent: profile="https://agent.example/profile" + +{ + "payment": { + "instruments": [ + { + "id": "pi_001", + "handler_id": "razorpay_upi_circle", + "type": "upi_circle", + "delegate_id": "c894aa29a7da69", + "selected": true, + "display": { + "name": "UPI Autopay", + "masked_vpa": "roh***@icici" + }, + "credential": { + "type": "upi_circle_cryptogram", + "cryptogram": "a345345dfgdfasdfh45jtyhgjkyutsdasd2", + "delegate_id": "c894aa29a7da69", + "expires_at": "2026-03-26T12:15:00Z" + } + } + ] + }, + "risk_signals": { + "ip_address": "203.0.113.42", + "user_agent": "Mozilla/5.0..." + } +} +``` + +#### Step 5: Handle Response + +The Complete Checkout response falls into one of four shapes: + +**Synchronous success** — Business responds directly with `completed`: + +```json +{ + "id": "checkout_abc123", + "status": "completed", + "order": { + "id": "order_xyz789", + "permalink_url": "https://store.com/orders/xyz789" + } +} +``` + +**Async processing** — Business returns `complete_in_progress`. Platform **MUST** poll `GET /checkout-sessions/{id}` until a terminal status is reached: + +```json +{ + "id": "checkout_abc123", + "status": "complete_in_progress" +} +``` + +**Failure** — Business returns `canceled` with structured `messages`: + +```json +{ + "id": "checkout_abc123", + "status": "canceled", + "messages": [ + { + "type": "recoverable", + "code": "cryptogram_expired", + "message": "The cryptogram has expired. Please retry." + } + ] +} +``` + +**Escalation** — Business returns `requires_escalation` (e.g. partial delegation awaiting primary holder approval): + +```json +{ + "id": "checkout_abc123", + "status": "requires_escalation", + "escalation": { + "type": "upi_circle_pending", + "message": "Awaiting approval from primary account holder." + } +} +``` + +#### Polling Logic + +When `complete_in_progress` or `requires_escalation` is received, the platform +**MUST** poll with exponential backoff. Terminal statuses are `completed` and +`canceled`. Maximum polling window is **2 minutes**. + +```javascript +const POLL_CONFIG = { + initialDelayMs: 2000, // start at 2 s + maxDelayMs: 10000, // cap at 10 s + backoffFactor: 1.5, // multiply delay each round + timeoutMs: 120000, // 2-minute hard stop +}; + +async function pollCheckout(checkoutId) { + const deadline = Date.now() + POLL_CONFIG.timeoutMs; + let delayMs = POLL_CONFIG.initialDelayMs; + + while (Date.now() < deadline) { + await sleep(delayMs); + + let checkout; + try { + checkout = await getCheckout(checkoutId); + } catch (err) { + // Network error — retry without advancing backoff + continue; + } + + switch (checkout.status) { + case "completed": + return { success: true, order: checkout.order }; + + case "canceled": + return { success: false, messages: checkout.messages }; + + case "complete_in_progress": + case "requires_escalation": + // Non-terminal — keep polling + break; + + default: + // Unknown status — treat as non-terminal, keep polling + break; + } + + // Advance backoff, capped at maxDelayMs + delayMs = Math.min(delayMs * POLL_CONFIG.backoffFactor, POLL_CONFIG.maxDelayMs); + } + + return { success: false, reason: "timeout", checkoutId }; +} +``` + +#### Error Handling After Poll + +| `messages[].type` | Action | +| :---------------- | :----- | +| `recoverable` | Fetch a fresh cryptogram and retry Complete Checkout (max 2 retries) | +| `requires_buyer_input` | Surface `messages[].message` to buyer (e.g. limit exceeded, mandate expired) | +| `timeout` (poll result) | Show generic failure; do **not** retry automatically | + +> For `recoverable` retries, always call the cryptogram API again — never reuse +> the previous cryptogram, even if its `expired_at` has not yet passed. + +--- + +## PSP Integration (Razorpay) + +### Delegated Debit Processing + +When the business submits a `upi_circle` instrument with a +`upi_circle_cryptogram` credential, Razorpay PSP: + +1. **Resolves the mandate** — looks up the UMN (Unique Mandate Number) from `delegate_id`. +2. **Validates the cryptogram** — confirms it is valid, unexpired, and matches the delegation. +3. **Initiates delegated debit via NPCI** — sends a debit request within the mandate's + configured limits. No MPIN required from the buyer. +4. **Receives NPCI response** — SUCCESS, FAILURE, or PENDING. +5. **Sends webhook to business** — `payment.authorized` or `payment.failed`. + +### Razorpay AI Commerce TSP as a Credential Provider: Delegation Setup and Cryptogram Issuance + +Razorpay acts as the **Credential Provider** — analogous to Google Pay's token +API or Stripe's Shared Payment Token (SPT) API. The cryptogram issuance +operation accepts a `delegate_id` and returns a single-use cryptogram: + +| Operation | Method | Purpose | +| :--------------------------- | :----- | :--------------------------- | +| Delegation Setup | POST | Delegating the payment mandate to the platform | +| Cryptogram issuance | POST | Issue a one-time cryptogram | + + + +**Response:** + +```json +{ + "delegate_id": "c894aa29a7da69", + "cryptogram_value": "a345345dfgdfasdfh45jtyhgjkyutsdasd2", + "expired_at": 1748716199 +} +``` + +The cryptogram is: +- **Single-use** — rejected on second attempt by NPCI. +- **Time-limited** — expires per `expired_at`. +- **Delegation-scoped** — valid only for the specific `delegate_id`. +- **Not merchant-scoped** — merchant identity comes from the payment request, + not the token (differs from Stripe SPT). + +--- + +## Error Handling + +| NPCI / Mandate Error | UCP `code` | `severity` | Platform Action | +| :------------------------------------ | :----------------- | :-------------------- | :---------------------------------------------------- | +| Mandate limit exceeded (per-txn) | `payment_declined` | `requires_buyer_input`| Lower amount or use a different payment method | +| Mandate limit exceeded (monthly) | `payment_declined` | `requires_buyer_input`| Wait for limit reset or use a different payment method| +| Mandate expired | `payment_declined` | `requires_buyer_input`| Re-initiate delegation setup | +| Mandate delinked (buyer revoked) | `payment_declined` | `requires_buyer_input`| Re-initiate delegation setup | +| Cryptogram expired | `payment_declined` | `recoverable` | Fetch new cryptogram and retry | +| Cryptogram invalid / already used | `payment_declined` | `recoverable` | Fetch new cryptogram and retry | +| Insufficient balance | `payment_declined` | `recoverable` | Suggest adding funds to primary account | +| Bank error | `payment_declined` | `recoverable` | Retry in 5 minutes | +| NPCI downtime | `payment_declined` | `recoverable` | Retry in 15 minutes | + +**Error Response Example:** + +```json +{ + "status": "canceled", + "messages": [ + { + "type": "error", + "code": "payment_declined", + "severity": "requires_buyer_input", + "content": "Monthly UPI delegation limit exceeded. Please increase your limit in your UPI app or use a different payment method.", + "path": "$.payment" + } + ] +} +``` + +--- + +## Security Considerations + +| Requirement | Description | +| :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Cryptogram single-use** | Each cryptogram authorizes exactly one debit. NPCI rejects reuse. Platform MUST fetch a fresh cryptogram per transaction. | +| **Cryptogram expiry** | Cryptograms are time-limited. Platform MUST check `expires_at` before submitting and fetch a new one if expired. | +| **Mandate limits** | Per-transaction and monthly limits are configured by the buyer at delegation setup and enforced by NPCI/bank. Cannot be overridden by platform or business.| +| **Mandate lifecycle** | Buyer can delink (revoke) the mandate at any time from their UPI app. Platform MUST handle `mandate expired` and `mandate delinked` errors gracefully. | +| **No PII in credential** | The cryptogram is an opaque authorization token. It does not contain VPA, account number, or any buyer PII. | +| **Data residency (RBI)** | All payment records and delegation data MUST be stored in India data centers per RBI data localization mandate. | +| **Webhook signature** | Business MUST verify `X-Razorpay-Signature` on every webhook via `HMAC_SHA256(body, webhook_secret)` before updating checkout state. | +| **Idempotency** | Business MUST ensure each cryptogram is processed exactly once to prevent double-charge. | +| **Delegation setup security** | Setup requires OTP verification + MPIN inside the buyer's UPI app. The platform never sees the buyer's MPIN or bank credentials. | +| **PCI-DSS scope** | Zero for all participants — no card data, no CVV/PIN at payment time. | +| **TLS/HTTPS only** | All API communication MUST use HTTPS with TLS 1.2+. | + +--- + +## Testing + +### End-to-End Test Checklist + +- [ ] Schemas pass JSON Schema draft 2020-12 validation. +- [ ] `snake_case` field names throughout. +- [ ] `const` type discriminators: `upi_circle` on instrument, `upi_circle_cryptogram` on credential. +- [ ] Instrument extends `payment_instrument.json` via `allOf`. +- [ ] Credential extends `payment_credential.json` via `allOf`. +- [ ] No `additionalProperties: false` on instrument or credential schemas. +- [ ] No base fields redeclared in extending schemas. +- [ ] `expires_at` uses RFC 3339 format. +- [ ] `delegate_id` required on instrument; `cryptogram` required on credential. +- [ ] Business profile at `/.well-known/ucp` declares `com.razorpay.upi_circle` with correct `key_id` and `environment`. +- [ ] Platform completes delegation setup and holds a `delegate_id` with `status: linked`. +- [ ] Platform calls Razorpay to fetch cryptogram immediately before Complete Checkout. +- [ ] Platform does **not** reuse a cryptogram across two transactions. +- [ ] Platform checks `expires_at` before submitting and fetches a fresh cryptogram if needed. +- [ ] Business processes cryptogram and responds `status: completed` — no escalation. +- [ ] Business receives `payment.authorized` webhook; verifies `X-Razorpay-Signature`. +- [ ] `payment.failed` webhook (limit exceeded / mandate revoked) triggers `canceled` with correct error. +- [ ] Platform handles `complete_in_progress` by polling until `completed` or `canceled`. +- [ ] Platform surfaces `requires_buyer_input` errors to buyer (mandate limit, mandate expired). +- [ ] Platform retries with fresh cryptogram on `recoverable` errors (cryptogram expired/invalid). +- [ ] `mkdocs build` completes without errors. + +--- + +## References + +| Resource | URL | +| :------------------------------- | :------------------------------------------------------------------------------------- | +| Razorpay Developer Docs | `https://razorpay.com/docs/` | +| UCP Checkout Specification | [checkout.md](checkout.md) | +| UCP Payment Handler Guide | [payment-handler-guide.md](payment-handler-guide.md) | +| UCP Payment Handler Template | [payment-handler-template.md](payment-handler-template.md) | +| Base `payment_instrument.json` | `https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/payment_instrument.json` | +| Base `payment_credential.json` | `https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/payment_credential.json` | +| NPCI UPI Circle / Delegation | `https://www.npci.org.in/what-we-do/upi/product-overview` | +| Stripe SPT (comparable pattern) | `https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens` | diff --git a/source/handlers/razorpay-upi-circle/schema.json b/source/handlers/razorpay-upi-circle/schema.json new file mode 100644 index 000000000..b6a09e714 --- /dev/null +++ b/source/handlers/razorpay-upi-circle/schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://razorpay.com/ucp/handlers/upi-circle/schema.json", + "title": "Razorpay UPI Circle Handler Schema", + "description": "Schema for the com.razorpay.upi_circle payment handler (UPI Circle / delegated payments flow). The platform collects the secondary user's delegate VPA and submits Complete Checkout with a upi_circle instrument and credential. The business calls Razorpay, creates an order, initiates a collect request using the delegate VPA, then returns requires_escalation with a upi_circle_credential (payment reference + expiry). The buyer authorizes in their PSP app; for partial delegation the primary account holder also approves. The business confirms via webhook.", + "name": "com.razorpay.upi_circle", + "version": "{{ ucp_version }}", + + "$defs": { + "upi_circle_instrument": { + "$ref": "types/upi_circle_instrument.json" + }, + + "com.razorpay.upi_circle": { + "payment_instrument": { + "title": "Razorpay UPI Circle Payment Instrument", + "description": "UPI Circle instrument for com.razorpay.upi_circle.", + "$ref": "#/$defs/upi_circle_instrument" + }, + + "platform_schema": { + "title": "Razorpay UPI Circle (Platform)", + "description": "Platform-level handler declaration for discovery.", + "allOf": [ + { + "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/payment_handler.json#/$defs/platform_schema" + }, + { + "type": "object", + "properties": { + "config": { + "$ref": "types/platform_config.json", + "description": "Platform configuration for com.razorpay.upi_circle." + } + } + } + ] + }, + + "business_schema": { + "title": "Razorpay UPI Circle (Business)", + "description": "Business-level handler declaration for discovery at /.well-known/ucp.", + "allOf": [ + { + "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/payment_handler.json#/$defs/business_schema" + }, + { + "type": "object", + "properties": { + "config": { + "$ref": "types/business_config.json", + "description": "Business configuration for com.razorpay.upi_circle." + } + } + } + ] + }, + + "response_schema": { + "title": "Razorpay UPI Circle (Response)", + "description": "Runtime handler configuration in checkout responses.", + "allOf": [ + { + "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/payment_handler.json#/$defs/response_schema" + }, + { + "type": "object", + "properties": { + "config": { + "$ref": "types/response_config.json", + "description": "Runtime configuration for this handler." + } + } + } + ] + } + } + } +} diff --git a/source/handlers/razorpay-upi-circle/types/business_config.json b/source/handlers/razorpay-upi-circle/types/business_config.json new file mode 100644 index 000000000..6a2d2927a --- /dev/null +++ b/source/handlers/razorpay-upi-circle/types/business_config.json @@ -0,0 +1,30 @@ + + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://razorpay.com/ucp/handlers/upi/types/business_config.json", + "title": "Razorpay UPI Business Config", + "description": "Business-level configuration for the com.razorpay.upi_circle payment handler (UPI Circle flow). Declared in the business's UCP profile at /.well-known/ucp.", + "type": "object", + "required": ["environment", "key_id"], + "properties": { + "environment": { + "type": "string", + "enum": ["sandbox", "production"], + "description": "Razorpay API environment. Use 'sandbox' for test mode (key_id starts with rzp_test_), 'production' for live mode (key_id starts with rzp_live_)." + }, + "key_id": { + "type": "string", + "description": "Razorpay public API key (rzp_test_* or rzp_live_*). Safe to advertise in the UCP profile. The key_secret MUST remain on the business backend and MUST NOT be included here.", + "pattern": "^rzp_(test|live)_[A-Za-z0-9]+$" + }, + "merchant_name": { + "type": "string", + "description": "Business display name shown to the buyer during the UPI payment flow." + }, + "currency": { + "type": "string", + "description": "Currency accepted by this handler. Defaults to 'INR' — UPI only supports INR transactions.", + "default": "INR" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/source/handlers/razorpay-upi-circle/types/platform_config.json b/source/handlers/razorpay-upi-circle/types/platform_config.json new file mode 100644 index 000000000..919349b52 --- /dev/null +++ b/source/handlers/razorpay-upi-circle/types/platform_config.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://razorpay.com/ucp/handlers/upi/types/platform_config.json", + "title": "Razorpay UPI Platform Config", + "description": "Platform-level configuration for the com.razorpay.upi_circle payment handler (UPI Circle flow). Declared in the platform's UCP profile.", + "type": "object", + "required": ["environment"], + "properties": { + "environment": { + "type": "string", + "enum": ["sandbox", "production"], + "description": "Razorpay API environment. Must match the business config environment ('sandbox' for test mode, 'production' for live mode)." + }, + "upi_apps": { + "type": "array", + "description": "UPI apps the platform is capable of deep-linking to (e.g., ['gpay', 'phonepe', 'paytm', 'bhim']).", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/source/handlers/razorpay-upi-circle/types/response_config.json b/source/handlers/razorpay-upi-circle/types/response_config.json new file mode 100644 index 000000000..3c3bd7bae --- /dev/null +++ b/source/handlers/razorpay-upi-circle/types/response_config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/response_config.json", + "title": "Razorpay UPI Circle Response Config", + "description": "Runtime handler configuration returned in the checkout response for com.razorpay.upi_circle. Because this handler follows the credential-before-Complete-Checkout pattern, no per-transaction limits are included here — mandate limits are enforced by NPCI and the buyer's bank at the delegation layer, not at the UCP config layer.", + "type": "object", + "required": ["environment"], + "properties": { + "environment": { + "type": "string", + "enum": ["test", "production"], + "description": "Razorpay API environment for this checkout." + } + } +} diff --git a/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json new file mode 100644 index 000000000..f8fe7a5ba --- /dev/null +++ b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/upi_circle_instrument.json", + "title": "Razorpay UPI Circle Instrument", + "description": "Handler-level wrapper for the upi_circle instrument type used with com.razorpay.upi_circle. Extends the base UPI Circle instrument schema and defines the available_upi_circle_instrument declaration for use in handler available_instruments.", + + "$defs": { + "available_upi_circle_instrument": { + "title": "Available UPI Circle Instrument", + "description": "Declares support for the UPI Circle flow in handler available_instruments. The buyer must have an active delegation mandate (delegate_id with status linked) obtained via Razorpay delegation setup.", + "allOf": [ + { "$ref": "https://ucp.dev/schemas/shopping/types/available_payment_instrument.json" } + ], + "type": "object", + "properties": { + "type": { + "const": "upi_circle" + } + } + } + }, + + "allOf": [ + { "$ref": "https://ucp.dev/schemas/shopping/types/upi_circle_instrument.json" } + ] +} diff --git a/source/schemas/shopping/types/upi_circle_credential.json b/source/schemas/shopping/types/upi_circle_credential.json new file mode 100644 index 000000000..6f3e51a2c --- /dev/null +++ b/source/schemas/shopping/types/upi_circle_credential.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/upi_circle_credential.json", + "title": "UPI Circle Payment Credential", + "description": "One-time cryptogram for a delegated UPI payment. Fetched by the platform from Razorpay before Complete Checkout. The cryptogram authorizes a single debit within the delegation mandate limits — no per-transaction buyer interaction is required. Extends payment_credential.json in the same pattern as token_credential.json.", + "allOf": [ + { + "$ref": "payment_credential.json" + }, + { + "type": "object", + "required": ["type", "cryptogram"], + "properties": { + "type": { + "const": "upi_circle_cryptogram", + "description": "Discriminator for UPI Circle cryptogram credential." + }, + "cryptogram": { + "type": "string", + "minLength": 1, + "description": "One-time cryptogram value issued by Razorpay. Single-use and time-limited — authorizes exactly one delegated debit. NPCI rejects reuse. The platform MUST fetch a fresh cryptogram for each transaction." + }, + "delegate_id": { + "type": "string", + "description": "Reference to the delegation mandate this cryptogram was issued for. Enables the merchant PSP to route the payment correctly. Already present on the instrument — included here for routing convenience." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 expiry timestamp. Platform MUST check this before submitting Complete Checkout and fetch a new cryptogram if expired." + } + } + } + ] +} diff --git a/source/schemas/shopping/types/upi_circle_instrument.json b/source/schemas/shopping/types/upi_circle_instrument.json new file mode 100644 index 000000000..e9da51a6f --- /dev/null +++ b/source/schemas/shopping/types/upi_circle_instrument.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/upi_circle_instrument.json", + "title": "UPI Circle Payment Instrument", + "description": "Payment instrument for delegated UPI payments. The buyer has pre-authorized the platform to initiate payments within configured limits via a UPI delegation mandate. No per-transaction buyer interaction is required — the platform fetches a one-time cryptogram from Razorpay and submits it in Complete Checkout. This instrument follows the standard credential-before-Complete-Checkout pattern (same as com.google.pay and dev.shopify.shop_pay).", + "allOf": [ + { + "$ref": "payment_instrument.json" + }, + { + "type": "object", + "required": ["type", "delegate_id"], + "properties": { + "type": { + "const": "upi_circle", + "description": "Discriminator for UPI Circle (delegated UPI) instrument." + }, + "delegate_id": { + "type": "string", + "minLength": 1, + "description": "Persistent identifier for the buyer's delegation mandate, obtained during one-time delegation setup with Razorpay. Analogous to a card vault token — it references saved delegation credentials. Used by the platform to fetch a one-time cryptogram before each transaction." + }, + "display": { + "type": "object", + "description": "Display information for this UPI Circle instrument.", + "properties": { + "name": { + "type": "string", + "maxLength": 100, + "description": "Human-readable name for platform UI (e.g., 'UPI Autopay', 'Delegated UPI')." + }, + "logo": { + "type": "string", + "format": "uri", + "description": "HTTPS URL to UPI logo for platform UI." + }, + "delegator_name": { + "type": "string", + "maxLength": 100, + "description": "Display name of the buyer who authorized the delegation mandate (e.g., 'Rohit S.')." + }, + "masked_vpa": { + "type": "string", + "description": "Masked VPA of the delegator for display purposes (e.g., 'roh***@icici'). The full VPA is internal UPI routing — only the masked form should be surfaced in UI." + } + } + } + } + } + ] +}