From e66f3f9ae9f94e55fef1d10d36117fd89a956457 Mon Sep 17 00:00:00 2001 From: Paras Kathuria Date: Tue, 31 Mar 2026 11:15:39 +0530 Subject: [PATCH 1/6] feat: add Razorpay UPI Circle payment handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the com.razorpay.upi.circle handler for UPI Circle (delegated) payments, where a secondary user pays from a primary account holder's bank account within NPCI limits (₹5,000/txn, ₹15,000/month). Key differences from com.razorpay.upi (Intent): - Credential flows platform → business (delegate VPA), not business → platform - No deep link; buyer authenticates via biometrics/PIN in PSP app - Supports partial delegation (primary user also approves) - Poll timeout extended to 5 min to cover approval latency New files: - source/schemas/shopping/types/upi_circle_instrument.json - source/schemas/shopping/types/upi_circle_credential.json - source/handlers/razorpay-upi-circle/schema.json - source/handlers/razorpay-upi-circle/types/business_config.json - source/handlers/razorpay-upi-circle/types/platform_config.json - source/handlers/razorpay-upi-circle/types/response_config.json - source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json - docs/specification/razorpay-upi-circle-payment-handler.md --- .../razorpay-upi-circle-payment-handler.md | 830 ++++++++++++++++++ .../handlers/razorpay-upi-circle/schema.json | 79 ++ .../types/business_config.json | 36 + .../types/platform_config.json | 43 + .../types/response_config.json | 30 + .../types/upi_circle_instrument.json | 26 + .../shopping/types/upi_circle_credential.json | 47 + .../shopping/types/upi_circle_instrument.json | 42 + 8 files changed, 1133 insertions(+) create mode 100644 docs/specification/razorpay-upi-circle-payment-handler.md create mode 100644 source/handlers/razorpay-upi-circle/schema.json create mode 100644 source/handlers/razorpay-upi-circle/types/business_config.json create mode 100644 source/handlers/razorpay-upi-circle/types/platform_config.json create mode 100644 source/handlers/razorpay-upi-circle/types/response_config.json create mode 100644 source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json create mode 100644 source/schemas/shopping/types/upi_circle_credential.json create mode 100644 source/schemas/shopping/types/upi_circle_instrument.json 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..ebcb286ee --- /dev/null +++ b/docs/specification/razorpay-upi-circle-payment-handler.md @@ -0,0 +1,830 @@ + + +# Razorpay UPI Circle Payment Handler + +* **Handler Name:** `com.razorpay.upi.circle` +* **Version:** `{{ ucp_version }}` + +## Introduction + +This handler enables **UPI Circle** (delegated) payments in India via +[Razorpay](https://razorpay.com) as the payment service provider. UPI Circle is +an NPCI feature that lets a **primary account holder** authorise a **secondary +(delegate) user** to make UPI payments from the primary user's bank account, +without the secondary user needing their own bank account or knowing the +primary user's MPIN. + +**UPI Circle flow** — The platform collects the secondary user's delegate VPA +and submits a Complete Checkout request with a `upi_circle` instrument and +credential containing that VPA. The business creates a Razorpay payment and +sends a collect request to NPCI via Razorpay. NPCI routes the request to the +secondary user's PSP app. The secondary user authenticates in their app using +biometrics or app PIN (no bank MPIN required). For **partial delegation**, +the primary account holder also approves in their PSP app. NPCI debits the +primary user's bank account and credits the merchant's bank. Razorpay notifies +the business via webhook and the business marks the checkout `completed`. + +### UPI Circle vs UPI Intent — Key Differences + +| Aspect | UPI Intent | UPI Circle | +| :------------------------------ | :-------------------------------------- | :---------------------------------------------- | +| **Credential direction** | Business → Platform (intent_uri) | Platform → Business (delegate VPA) | +| **Who authenticates** | Buyer via their own UPI app (MPIN) | Secondary user via biometrics / app PIN | +| **Bank account debited** | Buyer's own bank account | Primary account holder's bank account | +| **Platform action** | Open `upi://` deep link or render QR | Show VPA input + pending authorization screen | +| **NPCI per-txn limit** | No special limit | ₹5,000 per transaction | +| **NPCI monthly limit** | No special limit | ₹15,000 per month (per circle link) | +| **Additional approver** | None | Primary user (partial delegation only) | +| **Biometric auth** | Optional (PSP-dependent) | Mandatory — no MPIN path for secondary user | + +### Key Benefits + +* **No Razorpay SDK on platform** — The platform only needs to render a VPA + input field and poll for completion. No PSP SDK required. +* **PSP-agnostic instrument** — The `upi_circle` instrument and credential + schemas are NPCI-standard; any UPI-Circle-enabled PSP can implement this handler. +* **Zero PCI-DSS scope** — UPI is VPA-based. No card numbers involved. +* **Family-friendly payments** — Enables children, dependants, or employees to + pay on behalf of a primary account holder within strict NPCI limits. +* **Backward-compatible escalation** — Platforms that do not implement this + handler fall back to `continue_url` automatically. + +### Integration Guide + +| Participant | Integration Section | +| :----------- | :-------------------------------------------- | +| **Business** | [Business Integration](#business-integration) | +| **Platform** | [Platform Integration](#platform-integration) | + +--- + +## UPI Circle Concepts + +### Participants in a UPI Circle + +| Role | Description | +| :--------------------------- | :---------------------------------------------------------------------------------------------------------- | +| **Primary Account Holder** | The bank account owner who creates the UPI Circle. Sets delegation type and spending limits. Receives all transaction notifications. | +| **Secondary / Delegate User**| The person authorised to make payments from the primary user's account. Authenticates using biometrics or PSP app PIN. Cannot be linked to more than one primary user simultaneously. | +| **PSP App** | The UPI app used by both primary and secondary users (BHIM, PhonePe, Paytm, etc.). Enforces circle logic and authentication. | + +### Delegation Types + +| Type | Who Authorizes | Per-Transaction Max | Monthly Max | +| :---------- | :--------------------------- | :------------------ | :----------- | +| **Full** | Secondary user only | ₹5,000 | ₹15,000 | +| **Partial** | Secondary user + Primary user| ₹5,000 | ₹15,000 | + +> **New Circle Link Restriction:** During the first 24 hours after a new +> UPI Circle link is established, the per-transaction and monthly limits +> are reduced to ₹5,000 combined (NPCI mandated warm-up period). + +### Pre-Condition: Circle Must Be Set Up + +UPI Circle setup happens **out-of-band** — entirely within the primary user's +PSP app, before any merchant payment. The merchant and Razorpay are not +involved in this step. + +``` +[Primary Account Holder — Out of Band Setup] + +1. Primary user opens their PSP app (BHIM / PhonePe / Paytm / etc.) +2. Navigates to UPI Circle → Create Circle +3. Adds secondary user via their UPI ID or QR code scan +4. Selects delegation type: Full or Partial +5. Sets spending limits (up to NPCI maximums) +6. Secondary user receives invitation in their PSP app +7. Secondary user accepts the invitation +8. UPI Circle is now active — secondary user's delegate VPA + is linked to the primary user's bank account +``` + +--- + +## End-to-End Payment Flow + +### Actors + +| Actor | Role in Payment | +| :-------------------------------- | :---------------------------------------------------------------------------------------------- | +| **Customer (Secondary User)** | The buyer making the payment using their delegate VPA. | +| **Platform** | The shopping/commerce app — collects delegate VPA, submits checkout, polls for completion. | +| **Business** | The merchant backend — receives checkout, calls Razorpay, handles webhook, marks completed. | +| **Razorpay** | Payment gateway — creates order, sends collect request to NPCI, fires webhook on capture. | +| **NPCI UPI Network** | Routes collect request to secondary user's PSP; processes debit/credit via member banks. | +| **Secondary User's PSP App** | Displays authorization request; authenticates secondary user via biometrics/PIN. | +| **Primary User's PSP App** | (Partial delegation only) Receives approval request from secondary user's PSP; primary user approves. | +| **Remitter/Issuer Bank** | Primary account holder's bank — source of funds. | +| **Beneficiary Bank** | Merchant's bank — receives the credit. | + +### Flow Diagram + +``` ++------------+ +------------------+ +------------+ +----------+ +| Platform | | Business | | Razorpay | | NPCI | ++-----+------+ +--------+---------+ +------+-----+ +----+-----+ + | | | | + ════╪═══════════════════════╪════════════════════════╪═════════════════╪════ + PRE-CONDITION: Primary account holder has already created UPI Circle + delegation in their PSP app (out-of-band, no merchant involvement) + ════╪═══════════════════════╪════════════════════════╪═════════════════╪════ + | | | | + | 1. GET /.well-known/ | | | + | ucp | | | + |---------------------->| | | + | 2. Handler config | | | + | (env, merchant_ | | | + | name, limits) | | | + |<----------------------| | | + | | | | + | 3. POST /checkout | | | + | (create) | | | + |---------------------->| | | + | 4. Checkout response | | | + | (upi_circle | | | + | available, | | | + | per_txn_limit, | | | + | monthly_limit) | | | + |<----------------------| | | + | | | | + [Platform renders UPI Circle option with VPA input field] + [Buyer enters their delegate VPA, e.g. buyer@paytm] + | | | | + | 5. POST checkout/ | | | + | complete | | | + | instrument: | | | + | type: upi_circle | | | + | credential: | | | + | type: upi_circle | | | + | delegate_vpa: | | | + | buyer@paytm | | | + |---------------------->| | | + | | 6. POST /v1/orders | | + | |----------------------->| | + | | 7. { order_id } | | + | |<-----------------------| | + | | 8. POST /v1/payments/ | | + | | create/upi | | + | | (method: upi, | | + | | vpa: buyer@paytm, | | + | | order_id) | | + | |----------------------->| | + | | | 9. Collect | + | | | Request | + | | | (VPA: buyer@ | + | | | paytm) | + | | |---------------->| + | | 10. { payment_id, | | + | | status: created } | | + | |<-----------------------| | + | | | | + | 11. requires_ | | | + | escalation | | | + | code: requires_ | | | + | buyer_ | | | + | authentication | | | + | credential: | | | + | type: upi_circle | | | + | delegate_vpa: | | | + | buyer@paytm | | | + | razorpay_payment_ | | | + | id: pay_xxx | | | + | expires_at: ... | | | + |<----------------------| | | + | | | | + [Platform shows "Open your UPI app and authorize the payment"] + [Polling begins] + | | | | + : (polling loop) : : : + | | | | + ══════════════ NPCI routes collect request to secondary user's PSP ═══ + | | +---------------------------+ | + | | | Secondary User's PSP App | | + | | +-----------+---------------+ | + | | | | + | | 12. Push notification: | + | | "Authorize ₹X to | + | | " | + | | |<------------------+ + | | | | + | | [Secondary user sees | + | | payment request in app] | + | | [Authenticates via | + | | biometrics / app PIN] | + | | [No bank MPIN required] | + | | | | + ══════ Full Delegation path (secondary user approval is sufficient) ══ + | | 13. Authorization | + | | confirmed | + | | +----------------->| + | | | | + ══════ Partial Delegation path (primary user must also approve) ══════ + | | +---------------------------+ | + | | | Primary User's PSP App | | + | | +-----------+---------------+ | + | | 13a. PSP sends approval | + | | request to primary user | + | | | | + | | [Primary user approves or | + | | rejects in their PSP app] | + | | 13b. Primary user approves | + | | +----------------->| + ══════════════════════════════════════════════════════════════════════ + | | | | + | | | 14. NPCI debits | + | | | primary user's | + | | | bank (Remitter)| + | | |<--------------->| + | | | 15. NPCI credits| + | | | merchant bank | + | | | (Beneficiary) | + | | |<--------------->| + | | | | + | | 16. Webhook: | | + | | payment.captured | | + | |<-----------------------| | + | | | | + | | 17. Verify signature | | + | | Verify order_id | | + | | Capture payment | | + | | Mark checkout | | + | | completed | | + | | | | + | 18. GET /checkout | | | + | (poll) | | | + |---------------------->| | | + | 19. status: completed | | | + |<----------------------| | | +``` + +--- + +## 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. **Retrieve API keys** from *Settings → API Keys*: + * `key_id` — Public key (`rzp_live_*` or `rzp_test_*`). + * `key_secret` — Private key. **MUST remain on the business backend.** + **MUST NOT appear in any UCP profile, response, or client-side code.** +4. **Enable UPI** as an accepted payment method in the Razorpay dashboard + (*Settings → Payment Methods → UPI*). +5. **Configure webhooks** at *Settings → Webhooks* for the + `payment.captured` event (and optionally `payment.failed`). + +> **Note on NPCI Limits:** The business **MUST NOT** advertise this handler +> for checkout totals exceeding ₹5,000 (500,000 paise). Razorpay will reject +> the payment at the NPCI level if the amount exceeds the per-transaction limit. + +### Handler Configuration + +#### Handler Schema + +**Schema URL:** `https://razorpay.com/ucp/handlers/upi-circle/schema.json` + +| Config Variant | Context | Key Fields | +| :---------------- | :------------------- | :----------------------------------------------------- | +| `business_config` | Business discovery | `key_id`, `environment`, `merchant_name`, `max_amount` | +| `platform_config` | Platform discovery | `environment`, `vpa_input`, `poll_interval_ms` | +| `response_config` | Checkout responses | `environment`, `merchant_name`, `per_txn_limit_paise` | + +#### Business Config Fields + +| Field | Type | Required | Description | +| :-------------- | :------ | :------- | :--------------------------------------------------------------------- | +| `environment` | string | Yes | `sandbox` or `production` | +| `key_id` | string | Yes | Public Razorpay API key | +| `merchant_name` | string | No | Display name shown in the buyer's PSP app during authorization | +| `currency` | string | No | Must be `INR`. Defaults to `INR`. | +| `max_amount` | integer | No | Max accepted amount in paise. Cannot exceed 500000. Defaults to 500000.| + +#### Example Business Handler Declaration + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "payment_handlers": { + "com.razorpay.upi.circle": [ + { + "id": "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", + "key_id": "rzp_live_XXXXXXXXXXXXXXX", + "merchant_name": "Acme Store", + "max_amount": 500000 + } + } + ] + } + } +} +``` + +### Processing Payments + +When the business receives a Complete Checkout request with a `upi_circle` +instrument and `upi_circle` credential containing a `delegate_vpa`, it **MUST**: + +1. **Validate handler.** Confirm `instrument.handler_id` matches a declared + `com.razorpay.upi.circle` handler and `instrument.type == "upi_circle"`. + +2. **Validate VPA format.** Verify `credential.delegate_vpa` matches the + pattern `[a-zA-Z0-9._-+]+@[a-zA-Z0-9]+`. Reject malformed VPAs early. + +3. **Validate amount.** Confirm the checkout total does not exceed ₹5,000 + (500,000 paise). Return `payment_failed` immediately if it does. + +4. **Ensure idempotency.** If this `checkout_id` already has a Razorpay + order and payment, return the existing escalation response. + +5. **Create a Razorpay Order** (`POST /v1/orders`): + + ```http + POST https://api.razorpay.com/v1/orders + Authorization: Basic base64(key_id:key_secret) + Content-Type: application/json + + { + "amount": 50000, + "currency": "INR", + "receipt": "checkout_abc123", + "payment_capture": 0 + } + ``` + + | Field | Description | + | :---------------- | :----------------------------------------------------------- | + | `amount` | Amount in **paise** (1 INR = 100 paise). Max 500000. | + | `currency` | Must be `"INR"` — UPI only supports INR. | + | `receipt` | The UCP `checkout.id` or internal order reference. | + | `payment_capture` | `0` = manual capture (business captures after webhook). | + + Response: `{ "id": "order_Abcdef1234XXXX", ... }` + +6. **Initiate UPI Collect via Razorpay** (`POST /v1/payments/create/upi`): + + ```http + POST https://api.razorpay.com/v1/payments/create/upi + Authorization: Basic base64(key_id:key_secret) + Content-Type: application/json + + { + "amount": 50000, + "currency": "INR", + "order_id": "order_Abcdef1234XXXX", + "method": "upi", + "vpa": "buyer@paytm", + "email": "buyer@example.com", + "contact": "+919876543210", + "description": "Order checkout_abc123", + "callback_url": "https://business.example.com/razorpay/callback" + } + ``` + + | Field | Description | + | :------------- | :---------------------------------------------------------------- | + | `method` | Must be `"upi"`. | + | `vpa` | The delegate VPA from `credential.delegate_vpa`. | + | `callback_url` | Razorpay posts payment status here (used alongside webhooks). | + + Response: `{ "razorpay_payment_id": "pay_XXXXXXXXXXXXXXXX", "next": [...] }` + + > Razorpay sends a UPI collect request to NPCI, which routes it to the + > buyer's PSP app. The PSP recognizes the VPA as belonging to a UPI Circle + > delegation and presents the appropriate authorization UI. + +7. **Return `requires_escalation`** with `requires_buyer_authentication`: + + ```json + { + "status": "requires_escalation", + "continue_url": "https://business.example.com/checkout-sessions/checkout_abc123", + "messages": [ + { + "type": "error", + "code": "requires_buyer_authentication", + "severity": "requires_buyer_review", + "content": "Open your UPI app to authorize this payment", + "path": "$.payment.instruments[0]" + } + ], + "payment": { + "instruments": [ + { + "id": "instr_1", + "handler_id": "razorpay_upi_circle", + "type": "upi_circle", + "selected": true, + "display": { + "name": "Pay via UPI Circle", + "logo": "https://upload.wikimedia.org/wikipedia/commons/f/fa/UPI-Logo.png" + }, + "credential": { + "type": "upi_circle", + "delegate_vpa": "buyer@paytm", + "razorpay_payment_id": "pay_XXXXXXXXXXXXXXXX", + "push_sent_at": "2026-03-31T10:00:00Z", + "expires_at": "2026-03-31T10:15:00Z" + } + } + ] + } + } + ``` + +8. **Receive Razorpay webhook.** On `payment.captured`: + - Verify `X-Razorpay-Signature` using `HMAC_SHA256(webhook_body, webhook_secret)`. + - Confirm `payment.order_id` matches the order created for this checkout. + - Capture the payment (`POST /v1/payments/{id}/capture`) if not auto-captured. + - Mark the UCP checkout as `completed`. + +#### Handling Partial Delegation Timeout + +For partial delegation, the primary account holder must approve the payment. +This can take several minutes. The business **MUST** configure the webhook +for `payment.failed` as well and handle timeout scenarios: + +- If `payment.failed` is received with `description: "Payment was cancelled by the user"`, + mark the checkout as `requires_escalation` again (allowing retry) or `canceled`. +- The escalation `expires_at` should be set to 5–15 minutes to cover approval time. + +#### Error Mapping + +| Condition | UCP Error | Action | +| :------------------------------------------------- | :--------------- | :----------------------------------------------- | +| Checkout total exceeds ₹5,000 | `payment_failed` | Reject at handler selection — do not call Razorpay | +| Invalid or unresolvable delegate VPA | `payment_failed` | Return error; prompt buyer to re-enter VPA | +| Order/payment creation fails | `payment_failed` | Return error; platform may retry | +| Webhook `payment.failed` — buyer rejected | `payment_failed` | Retry or `canceled` | +| Webhook `payment.failed` — primary user rejected | `payment_failed` | Return error with partial delegation context | +| Authorization timed out (>15 min) | `payment_failed` | Business must create a new order | +| VPA not enrolled in UPI Circle | `payment_failed` | Return error; buyer must set up circle first | +| Currency is not `INR` | `payment_failed` | Reject at handler selection stage | + +--- + +## Platform Integration + +### Prerequisites + +Platforms do **not** need a Razorpay account. The platform only needs to: + +1. Render a VPA input field to collect the buyer's delegate UPI ID. +2. Handle the `requires_buyer_authentication` error code in the + `requires_escalation` response. +3. Display a "pending authorization" screen instructing the buyer to open + their UPI app (no deep link needed — the push notification is automatic). +4. Poll for checkout completion. + +### Handler Configuration + +#### Example Platform Handler Declaration + +```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", + "vpa_input": { + "placeholder": "Enter UPI Circle ID (e.g. name@upi)", + "label": "UPI Circle ID" + }, + "poll_interval_ms": 3000, + "poll_timeout_ms": 300000 + } + } + ] + } + } +} +``` + +### Payment Protocol + +#### Step 1: Discover Handler + +The platform identifies `com.razorpay.upi.circle` in the business's profile +and confirms `upi_circle` in `available_instruments`. + +Check that the checkout total does not exceed `response_config.per_txn_limit_paise` +(₹5,000). If it does, do not offer UPI Circle as a payment option. + +#### Step 2: Create / Fetch Checkout + +The platform creates or fetches the checkout session. The business returns +`response_config` with `environment`, `merchant_name`, and NPCI limits. + +```json +{ + "id": "checkout_abc123", + "status": "incomplete", + "currency": "INR", + "totals": [ + { "type": "subtotal", "amount": 49000 }, + { "type": "tax", "amount": 1000 }, + { "type": "total", "amount": 50000 } + ], + "ucp": { + "payment_handlers": { + "com.razorpay.upi.circle": [ + { + "id": "razorpay_upi_circle", + "version": "{{ ucp_version }}", + "available_instruments": [{ "type": "upi_circle" }], + "config": { + "environment": "production", + "merchant_name": "Acme Store", + "per_txn_limit_paise": 500000, + "monthly_limit_paise": 1500000 + } + } + ] + } + } +} +``` + +#### Step 3: Collect Delegate VPA from Buyer + +The platform renders a VPA input. The buyer enters their delegate UPI ID +(the UPI ID they have registered as a secondary user in a UPI Circle). + +``` +┌─────────────────────────────────────────────────────┐ +│ Pay via UPI Circle │ +│ │ +│ UPI Circle ID │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ Enter UPI Circle ID (e.g. name@upi) │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +│ ℹ️ Limit: ₹5,000 per transaction · ₹15,000/month │ +│ │ +│ [Pay ₹500.00] │ +└─────────────────────────────────────────────────────┘ +``` + +#### Step 4: Submit Complete Checkout (with VPA credential) + +The platform submits Complete Checkout with the `upi_circle` instrument +**and** the delegate VPA as the credential. Unlike UPI Intent, the credential +is provided by the buyer, not generated by the business. + +```http +POST /checkout-sessions/{checkout_id}/complete +UCP-Agent: profile="https://platform.example/profile" +Content-Type: application/json + +{ + "payment": { + "instruments": [ + { + "id": "instr_1", + "handler_id": "razorpay_upi_circle", + "type": "upi_circle", + "display": { + "name": "Pay via UPI Circle" + }, + "credential": { + "type": "upi_circle", + "delegate_vpa": "buyer@paytm" + } + } + ] + } +} +``` + +#### Step 5: Handle Escalation Response + +The business responds with `requires_escalation` and +`requires_buyer_authentication`. Unlike UPI Intent, there is **no deep link +to open** — the push notification was already sent to the buyer's PSP app. + +```javascript +const response = await completeCheckout(checkoutId, instrument, credential); + +if (response.status === "requires_escalation") { + const authError = response.messages.find( + m => m.code === "requires_buyer_authentication" && + m.severity === "requires_buyer_review" + ); + const instrument = response.payment?.instruments?.[0]; + const cred = instrument?.credential; + + if (authError && cred?.type === "upi_circle") { + // Check expiry before showing pending screen + if (cred.expires_at && new Date(cred.expires_at) < new Date()) { + fallbackToContinueUrl(response.continue_url); + return; + } + + // Show "check your UPI app" screen (no deep link needed) + showUpiCirclePendingScreen({ + vpa: cred.delegate_vpa, + expiresAt: cred.expires_at, + merchantName: handlerConfig.merchant_name, + }); + + // Start polling — allow up to 5 minutes for partial delegation approval + pollCheckoutStatus(checkoutId, response.continue_url); + } else { + fallbackToContinueUrl(response.continue_url); + } +} +``` + +#### Step 6: Poll for Completion + +After displaying the pending screen, the platform polls until `completed`, +`canceled`, or timeout: + +```javascript +async function pollCheckoutStatus(checkoutId, continueUrl) { + const POLL_INTERVAL_MS = 3000; + const MAX_POLLS = 100; // ~5 minutes (covers partial delegation approval) + + for (let i = 0; i < MAX_POLLS; i++) { + await sleep(POLL_INTERVAL_MS); + const checkout = await getCheckout(checkoutId); + + if (checkout.status === "completed") { + showSuccessScreen(checkout); + return; + } + if (checkout.status === "canceled") { + showFailureScreen("Payment was declined or timed out."); + return; + } + } + + // Timeout — fall back to continue_url + fallbackToContinueUrl(continueUrl); +} +``` + +> **Partial delegation timeout:** Allow up to 5 minutes of polling +> (`poll_timeout_ms: 300000`). The primary account holder may need time +> to see and approve the notification on their device. + +--- + +## Security Considerations + +| Requirement | Description | +| :-------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| **key_secret never exposed** | The `key_secret` **MUST** remain on the business backend. **MUST NOT** appear in UCP profiles, responses, or client-side code. | +| **Webhook signature** | The business **MUST** verify `X-Razorpay-Signature` on every webhook using `HMAC_SHA256(body, webhook_secret)` before updating checkout state.| +| **Order-to-checkout binding** | The business **MUST** verify that `payment.order_id` in the webhook matches the order created for this checkout. | +| **Amount enforcement** | The business **MUST** reject any checkout exceeding ₹5,000 (500,000 paise) — the NPCI per-transaction limit for UPI Circle. | +| **VPA validation** | The business **MUST** validate `delegate_vpa` format before calling Razorpay. Invalid VPAs will cause Razorpay API errors. | +| **Idempotency** | If a `checkout_id` already has a payment order, return the existing escalation response without creating a new order. | +| **Credential expiry** | The platform **MUST NOT** display an expired pending screen. It **MUST** fall back to `continue_url` if `expires_at` has passed. | +| **continue_url fallback** | The platform **MUST** fall back to `continue_url` if: the handler is not implemented, the credential is expired, or polling times out. | +| **No primary user data exposure** | The business response **MUST NOT** include any information about the primary account holder. Only the delegate VPA and payment reference are safe to return. | +| **INR only** | UPI only supports INR. Businesses **MUST NOT** advertise this handler for non-INR checkouts. | +| **TLS/HTTPS only** | All traffic to Razorpay APIs and UCP endpoints **MUST** use TLS 1.2 or higher. | + +--- + +## Testing + +Razorpay provides a full sandbox environment for end-to-end testing. + +### Setup + +1. Toggle to **Test Mode** in the [Razorpay Dashboard](https://dashboard.razorpay.com). +2. From *Settings → API Keys*, generate test keys (`rzp_test_*`). +3. Use `environment: "sandbox"` in both business and platform configs. + +### Simulating UPI Circle in Sandbox + +In sandbox mode, use Razorpay test VPAs to simulate different scenarios: + +| Test VPA | Simulated Scenario | +| :-------------------- | :---------------------------------------- | +| `success@razorpay` | Payment authorized (full delegation) | +| `failure@razorpay` | Payment declined by secondary user | +| `partial@razorpay` | Partial delegation — primary approves | +| `timeout@razorpay` | Authorization times out after 15 seconds | + +> Contact Razorpay support for the current list of sandbox UPI Circle test VPAs, +> as these may be updated with new sandbox releases. + +### End-to-End Test Checklist + +- [ ] Business profile at `/.well-known/ucp` declares `com.razorpay.upi.circle` with `rzp_test_*` key and `environment: "sandbox"`. +- [ ] Platform profile declares `com.razorpay.upi.circle` with `environment: "sandbox"`. +- [ ] Platform does **not** offer UPI Circle when checkout total exceeds ₹5,000. +- [ ] Platform renders VPA input field with correct label and placeholder from `response_config`. +- [ ] Platform submits Complete Checkout with `upi_circle` instrument **and** `upi_circle` credential containing `delegate_vpa`. +- [ ] Business validates VPA format before calling Razorpay. +- [ ] Business creates Razorpay order via `POST /v1/orders` in test mode. +- [ ] Business initiates collect via `POST /v1/payments/create/upi` with the delegate VPA. +- [ ] Business returns `requires_escalation` with `requires_buyer_authentication` + `upi_circle_credential`. +- [ ] Escalation response includes `continue_url` (always required). +- [ ] `credential.razorpay_payment_id` is present and non-empty. +- [ ] `credential.expires_at` is in the future. +- [ ] Platform shows "check your UPI app" screen — does NOT open a deep link. +- [ ] Platform polls with 3-second interval, up to 5-minute timeout. +- [ ] Business receives `payment.captured` webhook; verifies `X-Razorpay-Signature`. +- [ ] Business marks checkout `completed`. +- [ ] Platform polling returns `status: completed`. +- [ ] Platform falls back to `continue_url` when credential is expired. +- [ ] Platform falls back to `continue_url` when polling times out. +- [ ] `payment.failed` webhook correctly triggers checkout cancellation. + +### Webhook Signature Verification Reference + +```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) +``` + +--- + +## Comparison with UPI Intent Handler + +| Aspect | `com.razorpay.upi` (Intent) | `com.razorpay.upi.circle` (Circle) | +| :---------------------------- | :---------------------------------------------- | :----------------------------------------------------- | +| **Instrument type** | `upi_intent` | `upi_circle` | +| **Platform submits credential?** | No — no credential on submit | Yes — delegate VPA | +| **Business generates credential?** | Yes — `intent_uri` in escalation response | No — returns payment reference only | +| **Platform action post-escalation** | Open `upi://` deep link or render QR | Show "check your app" screen, start polling | +| **Buyer auth in PSP app** | MPIN | Biometrics / app PIN (no MPIN) | +| **Additional approver** | None | Primary user (partial delegation only) | +| **Per-txn limit** | No special limit | ₹5,000 (NPCI) | +| **Monthly limit** | No special limit | ₹15,000 per circle link (NPCI) | +| **Recommended for** | Standard UPI payments by any buyer | Secondary users paying via delegated access | +| **Poll timeout** | ~1 minute | ~5 minutes (partial delegation needs more time) | + +--- + +## Applicability Beyond Razorpay + +The `requires_buyer_authentication` error code and `upi_circle` credential +pattern are PSP-agnostic. Any UPI PSP (PayU, Cashfree, Juspay) that supports +NPCI UPI Circle delegated payments can implement this same handler by +accepting the `delegate_vpa` credential and processing the collect request +through the NPCI network. + +--- + +## References + +* **Handler Spec:** `https://razorpay.com/ucp/handlers/upi-circle` +* **Handler Schema:** `https://razorpay.com/ucp/handlers/upi-circle/schema.json` +* **UPI Circle Instrument Schema:** [upi_circle_instrument.json](site:schemas/shopping/types/upi_circle_instrument.json) +* **UPI Circle Credential Schema:** [upi_circle_credential.json](site:schemas/shopping/types/upi_circle_credential.json) +* **Razorpay Orders API:** `https://razorpay.com/docs/api/orders/` +* **Razorpay Payments API:** `https://razorpay.com/docs/api/payments/` +* **Razorpay UPI Docs:** `https://razorpay.com/docs/payments/payment-methods/upi/` +* **NPCI UPI Circle Overview:** `https://www.npci.org.in/what-we-do/upi/product-overview` +* **UPI Intent Handler:** [razorpay-upi-payment-handler.md](razorpay-upi-payment-handler.md) +* **UCP Payment Handler Guide:** [payment-handler-guide.md](payment-handler-guide.md) +* **UCP Checkout Specification:** [checkout.md](checkout.md) diff --git a/source/handlers/razorpay-upi-circle/schema.json b/source/handlers/razorpay-upi-circle/schema.json new file mode 100644 index 000000000..7f928c8bf --- /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..beb6bb98b --- /dev/null +++ b/source/handlers/razorpay-upi-circle/types/business_config.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/business_config.json", + "title": "Razorpay UPI Circle Business Config", + "description": "Business-level configuration for the com.razorpay.upi.circle payment handler. Declared in the business's UCP profile at /.well-known/ucp. UPI Circle enables secondary users to pay from a primary account holder's bank account within NPCI-mandated delegation limits (₹5,000 per transaction, ₹15,000 per month).", + "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 Circle payment authorization in the PSP app." + }, + "currency": { + "type": "string", + "description": "Currency accepted by this handler. Defaults to 'INR' — UPI only supports INR transactions.", + "default": "INR" + }, + "max_amount": { + "type": "integer", + "description": "Maximum transaction amount in paise that this handler will accept. Must not exceed the NPCI UPI Circle per-transaction limit of ₹5,000 (500000 paise). Defaults to 500000 if not specified.", + "maximum": 500000, + "default": 500000 + } + }, + "additionalProperties": false +} 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..23c7f9004 --- /dev/null +++ b/source/handlers/razorpay-upi-circle/types/platform_config.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/platform_config.json", + "title": "Razorpay UPI Circle Platform Config", + "description": "Platform-level configuration for the com.razorpay.upi.circle payment handler. Declared in the platform's UCP profile. Unlike UPI Intent (which requires deep-link capability), UPI Circle requires the platform to collect the buyer's delegate VPA and display a pending authorization screen while the buyer authenticates in their PSP app.", + "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)." + }, + "vpa_input": { + "type": "object", + "description": "Configuration for the delegate VPA input field shown to the buyer.", + "properties": { + "placeholder": { + "type": "string", + "description": "Placeholder text for the VPA input (e.g., 'Enter your UPI ID'). Defaults to 'Enter UPI Circle ID (e.g. name@upi)'." + }, + "label": { + "type": "string", + "description": "Label for the VPA input field (e.g., 'UPI Circle ID'). Defaults to 'UPI Circle ID'." + } + }, + "additionalProperties": false + }, + "poll_interval_ms": { + "type": "integer", + "description": "How often (in milliseconds) the platform polls for checkout completion after submitting the VPA. Defaults to 3000 (3 seconds). Minimum 2000.", + "minimum": 2000, + "default": 3000 + }, + "poll_timeout_ms": { + "type": "integer", + "description": "Maximum time (in milliseconds) the platform will poll before falling back to continue_url. Defaults to 300000 (5 minutes) to account for partial delegation approval time.", + "minimum": 30000, + "default": 300000 + } + }, + "additionalProperties": false +} 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..5580f2ef0 --- /dev/null +++ b/source/handlers/razorpay-upi-circle/types/response_config.json @@ -0,0 +1,30 @@ +{ + "$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. After the platform submits Complete Checkout with a upi_circle instrument and the buyer's delegate VPA credential, the business submits a collect request to Razorpay and returns requires_escalation. This config provides display context and NPCI limit information the platform needs to render the pending authorization screen.", + "type": "object", + "required": ["environment"], + "properties": { + "environment": { + "type": "string", + "enum": ["sandbox", "production"], + "description": "Razorpay API environment for this checkout." + }, + "merchant_name": { + "type": "string", + "description": "Business display name shown to the buyer during UPI Circle authorization in the PSP app." + }, + "per_txn_limit_paise": { + "type": "integer", + "description": "Per-transaction limit in paise for UPI Circle payments. NPCI mandates a maximum of ₹5,000 (500000 paise). Informational — the platform may display this to the buyer.", + "default": 500000 + }, + "monthly_limit_paise": { + "type": "integer", + "description": "Monthly cumulative limit in paise for UPI Circle payments. NPCI mandates a maximum of ₹15,000 (1500000 paise). Informational — the platform may display this to the buyer.", + "default": 1500000 + } + }, + "additionalProperties": false +} 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..85d9a8744 --- /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. NPCI limits apply: ₹5,000 per transaction, ₹15,000 per month per UPI Circle link.", + + "$defs": { + "available_upi_circle_instrument": { + "title": "Available UPI Circle Instrument", + "description": "Declares support for the UPI Circle flow in handler available_instruments. The secondary user must have an active UPI Circle delegation from a primary account holder via their PSP.", + "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..273398574 --- /dev/null +++ b/source/schemas/shopping/types/upi_circle_credential.json @@ -0,0 +1,47 @@ +{ + "$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": "Credential for UPI Circle (delegated) payments. Used in two directions: (1) the platform submits the secondary user's delegate VPA to the business during Complete Checkout; (2) the business returns a payment reference in the escalation response after submitting the collect request to the PSP. The delegate_vpa is required on input; razorpay_payment_id and push_sent_at are populated by the business on output.", + "allOf": [ + { + "$ref": "payment_credential.json" + }, + { + "type": "object", + "required": ["type", "delegate_vpa"], + "properties": { + "type": { + "const": "upi_circle", + "description": "Discriminator for UPI Circle credential." + }, + "delegate_vpa": { + "type": "string", + "pattern": "^[a-zA-Z0-9.\\-_+]+@[a-zA-Z0-9]+$", + "minLength": 3, + "maxLength": 255, + "description": "The secondary (delegate) user's UPI Virtual Payment Address (VPA/UPI ID). This is the UPI ID of the buyer making the payment — it resolves to the primary account holder's bank account via the UPI Circle delegation. Example: 'buyer@paytm'." + }, + "delegation_type": { + "type": "string", + "enum": ["full", "partial"], + "description": "UPI Circle delegation type. 'full' means the secondary user can authorize payments independently (up to NPCI limits). 'partial' means each transaction also requires explicit approval from the primary account holder. If absent, the PSP determines the delegation type from the VPA's circle configuration." + }, + "razorpay_payment_id": { + "type": "string", + "description": "Razorpay payment identifier (e.g., 'pay_XXXXXXXXXXXXXXXX'). Populated by the business in the escalation response after the PSP collect request is initiated. Absent when submitted by the platform." + }, + "push_sent_at": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 timestamp indicating when the push notification was dispatched to the secondary user's PSP app. Populated by the business in the escalation response." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 expiry timestamp for this payment authorization window. Per NPCI best practice, UPI collect requests expire after 5–15 minutes. The platform MUST display a timeout indicator and fall back to continue_url if the credential expires before polling returns completed." + } + } + } + ] +} 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..f1d279df1 --- /dev/null +++ b/source/schemas/shopping/types/upi_circle_instrument.json @@ -0,0 +1,42 @@ +{ + "$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 UPI Circle (delegated) payments. Extends the base payment instrument for secondary users who pay via a UPI Circle delegation from a primary account holder's bank account. Unlike UPI Intent (where no credential is submitted and an intent URI is returned), UPI Circle requires the secondary user's delegate VPA as an input credential. The buyer authenticates via biometrics or PSP app PIN — no bank MPIN is required from the secondary user.", + "allOf": [ + { + "$ref": "payment_instrument.json" + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "const": "upi_circle", + "description": "Discriminator for UPI Circle instrument." + }, + "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., 'Pay via UPI Circle')." + }, + "logo": { + "type": "string", + "format": "uri", + "description": "HTTPS URL to UPI logo for platform UI." + }, + "description": { + "type": "string", + "maxLength": 255, + "description": "Optional description shown to the buyer (e.g., 'Pay using your linked UPI Circle account')." + } + } + } + } + } + ] +} From 8cd69dc1378657b307ae81248d5168f8d6e3de55 Mon Sep 17 00:00:00 2001 From: Paras Kathuria Date: Tue, 31 Mar 2026 11:36:00 +0530 Subject: [PATCH 2/6] refactor: align UPI Circle handler with protocol contribution spec Complete rewrite to match the credential-before-Complete-Checkout pattern: - Handler follows com.google.pay / dev.shopify.shop_pay architecture: platform fetches cryptogram from Razorpay AI Commerce TSP before submitting checkout; business responds straight to completed - upi_circle_instrument: use delegate_id (persistent mandate ref) not delegate_vpa; add display.delegator_name and display.masked_vpa - upi_circle_credential: type discriminator is upi_circle_cryptogram; fields are cryptogram + optional delegate_id + expires_at - business_config: simplified to business_id + environment only; removed key_id, merchant_name, max_amount, additionalProperties:false - platform_config: environment only; removed vpa_input, poll_* fields - response_config: environment only; removed per_txn_limit_paise / monthly_limit_paise (mandate limits are NPCI/bank-enforced, not UCP config) - spec doc: rewritten with delegation setup (out-of-band), TSP integration, PSP section, error handling table, and correct architecture comparison table vs UPI Intent --- .../razorpay-upi-circle-payment-handler.md | 1126 +++++++---------- .../types/business_config.json | 33 +- .../types/platform_config.json | 36 +- .../types/response_config.json | 21 +- .../types/upi_circle_instrument.json | 4 +- .../shopping/types/upi_circle_credential.json | 32 +- .../shopping/types/upi_circle_instrument.json | 23 +- 7 files changed, 531 insertions(+), 744 deletions(-) diff --git a/docs/specification/razorpay-upi-circle-payment-handler.md b/docs/specification/razorpay-upi-circle-payment-handler.md index ebcb286ee..7fdefa2bd 100644 --- a/docs/specification/razorpay-upi-circle-payment-handler.md +++ b/docs/specification/razorpay-upi-circle-payment-handler.md @@ -21,254 +21,218 @@ ## Introduction -This handler enables **UPI Circle** (delegated) payments in India via -[Razorpay](https://razorpay.com) as the payment service provider. UPI Circle is -an NPCI feature that lets a **primary account holder** authorise a **secondary -(delegate) user** to make UPI payments from the primary user's bank account, -without the secondary user needing their own bank account or knowing the -primary user's MPIN. - -**UPI Circle flow** — The platform collects the secondary user's delegate VPA -and submits a Complete Checkout request with a `upi_circle` instrument and -credential containing that VPA. The business creates a Razorpay payment and -sends a collect request to NPCI via Razorpay. NPCI routes the request to the -secondary user's PSP app. The secondary user authenticates in their app using -biometrics or app PIN (no bank MPIN required). For **partial delegation**, -the primary account holder also approves in their PSP app. NPCI debits the -primary user's bank account and credits the merchant's bank. Razorpay notifies -the business via webhook and the business marks the checkout `completed`. - -### UPI Circle vs UPI Intent — Key Differences - -| Aspect | UPI Intent | UPI Circle | -| :------------------------------ | :-------------------------------------- | :---------------------------------------------- | -| **Credential direction** | Business → Platform (intent_uri) | Platform → Business (delegate VPA) | -| **Who authenticates** | Buyer via their own UPI app (MPIN) | Secondary user via biometrics / app PIN | -| **Bank account debited** | Buyer's own bank account | Primary account holder's bank account | -| **Platform action** | Open `upi://` deep link or render QR | Show VPA input + pending authorization screen | -| **NPCI per-txn limit** | No special limit | ₹5,000 per transaction | -| **NPCI monthly limit** | No special limit | ₹15,000 per month (per circle link) | -| **Additional approver** | None | Primary user (partial delegation only) | -| **Biometric auth** | Optional (PSP-dependent) | Mandatory — no MPIN path for secondary user | +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, sends it in Complete Checkout, and the business +processes it straight to `completed`. No escalation, no app switch, no buyer +interaction at payment time. + +> **Note:** This handler requires a **one-time delegation setup** between the +> buyer, the AI Platform, and Razorpay AI Commerce TSP. This setup happens +> outside UCP as a bilateral integration — analogous to a buyer saving a card +> on ChatGPT. See [Delegation Setup](#delegation-setup-outside-ucp). + +### UPI Circle vs UPI Intent — Architecture Comparison + +| Property | UPI Intent (`com.razorpay.upi`) | UPI Circle (`com.razorpay.upi.circle`) | +| :------------------------------ | :-------------------------------------- | :----------------------------------------------- | +| **Per-transaction auth** | Yes — buyer enters MPIN every time | No — mandate pre-authorizes within limits | +| **Credential acquisition** | PSP-side (after Complete Checkout) | Platform-side (before Complete Checkout) | +| **UCP architecture fit** | Requires escalation / protocol amendment| Fits existing architecture — no changes needed | +| **Truly agentic** | No — buyer must interact per payment | Yes — agent pays autonomously | +| **Buyer experience** | App switch / QR scan per transaction | Zero-friction — like card-on-file | +| **UCP pattern analog** | Custom escalation flow | Same as `com.google.pay`, `dev.shopify.shop_pay` | ### Key Benefits -* **No Razorpay SDK on platform** — The platform only needs to render a VPA - input field and poll for completion. No PSP SDK required. -* **PSP-agnostic instrument** — The `upi_circle` instrument and credential - schemas are NPCI-standard; any UPI-Circle-enabled PSP can implement this handler. -* **Zero PCI-DSS scope** — UPI is VPA-based. No card numbers involved. -* **Family-friendly payments** — Enables children, dependants, or employees to - pay on behalf of a primary account holder within strict NPCI limits. -* **Backward-compatible escalation** — Platforms that do not implement this - handler fall back to `continue_url` automatically. +* **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 | Integration Section | -| :----------- | :-------------------------------------------- | -| **Business** | [Business Integration](#business-integration) | -| **Platform** | [Platform Integration](#platform-integration) | +| Participant | Section | +| :------------------- | :-------------------------------------------------------- | +| **Business** | [Business Integration](#business-integration) | +| **Platform** | [Platform Integration](#platform-integration) | +| **PSP (Razorpay)** | [PSP Integration](#psp-integration-razorpay) | --- -## UPI Circle Concepts +## Participants -### Participants in a UPI Circle +> **Note on Terminology:** This specification refers to the Razorpay merchant +> account holder as the **"business."** Technical schema fields retain standard +> industry nomenclature `merchant_*` where applicable. -| Role | Description | -| :--------------------------- | :---------------------------------------------------------------------------------------------------------- | -| **Primary Account Holder** | The bank account owner who creates the UPI Circle. Sets delegation type and spending limits. Receives all transaction notifications. | -| **Secondary / Delegate User**| The person authorised to make payments from the primary user's account. Authenticates using biometrics or PSP app PIN. Cannot be linked to more than one primary user simultaneously. | -| **PSP App** | The UPI app used by both primary and secondary users (BHIM, PhonePe, Paytm, etc.). Enforces circle logic and authentication. | +| Participant | Role | Prerequisites | +| :-------------------------- | :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------ | +| **Business** | Advertises handler configuration; processes cryptogram via Razorpay PSP; responds `completed`| Yes — Razorpay account with KYC, webhook endpoint (HTTPS, TLS 1.2+)| +| **Platform** | Discovers handler; fetches cryptogram from TSP; submits in Complete Checkout; polls if async | Yes — Razorpay AI Commerce TSP integration, active `delegate_id` | +| **PSP (Razorpay)** | Processes delegated debit via NPCI using mandate UMN; fires webhook to business | N/A — infrastructure provider | +| **Razorpay AI Commerce TSP**| Issues one-time cryptograms for established delegation mandates | N/A — credential provider (analogous to Google Pay API or Stripe SPT)| -### Delegation Types - -| Type | Who Authorizes | Per-Transaction Max | Monthly Max | -| :---------- | :--------------------------- | :------------------ | :----------- | -| **Full** | Secondary user only | ₹5,000 | ₹15,000 | -| **Partial** | Secondary user + Primary user| ₹5,000 | ₹15,000 | +--- -> **New Circle Link Restriction:** During the first 24 hours after a new -> UPI Circle link is established, the per-transaction and monthly limits -> are reduced to ₹5,000 combined (NPCI mandated warm-up period). +## Delegation Setup (Outside UCP) -### Pre-Condition: Circle Must Be Set Up +Before any payment can occur, the buyer must establish a delegation mandate. +This is a **one-time bilateral integration** between the AI Platform and +Razorpay AI Commerce TSP — it is **not part of UCP**, just as saving a card on +ChatGPT is not part of the checkout protocol. -UPI Circle setup happens **out-of-band** — entirely within the primary user's -PSP app, before any merchant payment. The merchant and Razorpay are not -involved in this step. +### Setup Flow ``` -[Primary Account Holder — Out of Band Setup] - -1. Primary user opens their PSP app (BHIM / PhonePe / Paytm / etc.) -2. Navigates to UPI Circle → Create Circle -3. Adds secondary user via their UPI ID or QR code scan -4. Selects delegation type: Full or Partial -5. Sets spending limits (up to NPCI maximums) -6. Secondary user receives invitation in their PSP app -7. Secondary user accepts the invitation -8. UPI Circle is now active — secondary user's delegate VPA - is linked to the primary user's bank account ++------------+ +---------------------------+ +-----------+ +| Platform | | Razorpay AI Commerce TSP | | Buyer UPI | ++-----+------+ +-------------+-------------+ | App | + | | +-----+-----+ + | | | + [Buyer initiates delegation setup on Platform] | + | | | + | 1. POST /customers/generate_otp | | + | { mobile: "+91XXXXXXXXXX" } | | + |----------------------------------->| | + | |-- OTP SMS to buyer's mobile ->| + | 2. { customer_id } | | + |<-----------------------------------| | + | | | + [Buyer enters OTP on Platform] | + | | | + | 3. POST /customers/{id}/otp_submit | | + | { otp: "XXXXXX" } | | + |----------------------------------->| | + | 4. { intent_link, qr_code } | | + |<-----------------------------------| | + | | | + [Platform opens intent link or shows QR — buyer opens in UPI app] | + | | | + | | 5. Buyer configures | + | | delegation limits, | + | | enters MPIN | + | |<------------------------------| + | | | + | | 6. Bank + NPCI confirm | + | | mandate created (UMN) | + | |<- - - - - - - - - - - - - - - | + | | | + | 7. GET /delegate/{delegate_id} | | + | (poll until status: linked) | | + |----------------------------------->| | + | 8. { status: "linked", | | + | delegate_id: "c894aa29..." } | | + |<-----------------------------------| | + | | | + [Platform stores delegate_id — used for all future payments] ``` ---- +### Setup API Reference -## End-to-End Payment Flow +| API | Method | Purpose | +| :---------------------------------------------- | :----- | :-------------------------------------------- | +| `/v1/upi/ai-tpap/customers/generate_otp` | POST | Send OTP to buyer's mobile | +| `/v1/upi/ai-tpap/customers/{cust_id}/otp_submit`| POST | Verify OTP; receive intent link / QR for delegation | +| `/v1/upi/ai-tpap/delegate/{delegate_id}` | GET | Poll delegation status until `linked` | -### Actors +**Output:** `delegate_id` — persistent mandate reference. All subsequent +payments use this ID to fetch cryptograms. -| Actor | Role in Payment | -| :-------------------------------- | :---------------------------------------------------------------------------------------------- | -| **Customer (Secondary User)** | The buyer making the payment using their delegate VPA. | -| **Platform** | The shopping/commerce app — collects delegate VPA, submits checkout, polls for completion. | -| **Business** | The merchant backend — receives checkout, calls Razorpay, handles webhook, marks completed. | -| **Razorpay** | Payment gateway — creates order, sends collect request to NPCI, fires webhook on capture. | -| **NPCI UPI Network** | Routes collect request to secondary user's PSP; processes debit/credit via member banks. | -| **Secondary User's PSP App** | Displays authorization request; authenticates secondary user via biometrics/PIN. | -| **Primary User's PSP App** | (Partial delegation only) Receives approval request from secondary user's PSP; primary user approves. | -| **Remitter/Issuer Bank** | Primary account holder's bank — source of funds. | -| **Beneficiary Bank** | Merchant's bank — receives the credit. | +> Detailed API specifications are maintained separately. See +> [Razorpay AI Commerce TSP API Docs](https://razorpay.com/docs/ucp/ai-tpap/). + +--- + +## End-to-End Payment Flow ### Flow Diagram ``` -+------------+ +------------------+ +------------+ +----------+ -| Platform | | Business | | Razorpay | | NPCI | -+-----+------+ +--------+---------+ +------+-----+ +----+-----+ - | | | | - ════╪═══════════════════════╪════════════════════════╪═════════════════╪════ - PRE-CONDITION: Primary account holder has already created UPI Circle - delegation in their PSP app (out-of-band, no merchant involvement) - ════╪═══════════════════════╪════════════════════════╪═════════════════╪════ - | | | | - | 1. GET /.well-known/ | | | - | ucp | | | - |---------------------->| | | - | 2. Handler config | | | - | (env, merchant_ | | | - | name, limits) | | | - |<----------------------| | | - | | | | - | 3. POST /checkout | | | - | (create) | | | - |---------------------->| | | - | 4. Checkout response | | | - | (upi_circle | | | - | available, | | | - | per_txn_limit, | | | - | monthly_limit) | | | - |<----------------------| | | - | | | | - [Platform renders UPI Circle option with VPA input field] - [Buyer enters their delegate VPA, e.g. buyer@paytm] - | | | | - | 5. POST checkout/ | | | - | complete | | | - | instrument: | | | - | type: upi_circle | | | - | credential: | | | - | type: upi_circle | | | - | delegate_vpa: | | | - | buyer@paytm | | | - |---------------------->| | | - | | 6. POST /v1/orders | | - | |----------------------->| | - | | 7. { order_id } | | - | |<-----------------------| | - | | 8. POST /v1/payments/ | | - | | create/upi | | - | | (method: upi, | | - | | vpa: buyer@paytm, | | - | | order_id) | | - | |----------------------->| | - | | | 9. Collect | - | | | Request | - | | | (VPA: buyer@ | - | | | paytm) | - | | |---------------->| - | | 10. { payment_id, | | - | | status: created } | | - | |<-----------------------| | - | | | | - | 11. requires_ | | | - | escalation | | | - | code: requires_ | | | - | buyer_ | | | - | authentication | | | - | credential: | | | - | type: upi_circle | | | - | delegate_vpa: | | | - | buyer@paytm | | | - | razorpay_payment_ | | | - | id: pay_xxx | | | - | expires_at: ... | | | - |<----------------------| | | - | | | | - [Platform shows "Open your UPI app and authorize the payment"] - [Polling begins] - | | | | - : (polling loop) : : : - | | | | - ══════════════ NPCI routes collect request to secondary user's PSP ═══ - | | +---------------------------+ | - | | | Secondary User's PSP App | | - | | +-----------+---------------+ | - | | | | - | | 12. Push notification: | - | | "Authorize ₹X to | - | | " | - | | |<------------------+ - | | | | - | | [Secondary user sees | - | | payment request in app] | - | | [Authenticates via | - | | biometrics / app PIN] | - | | [No bank MPIN required] | - | | | | - ══════ Full Delegation path (secondary user approval is sufficient) ══ - | | 13. Authorization | - | | confirmed | - | | +----------------->| - | | | | - ══════ Partial Delegation path (primary user must also approve) ══════ - | | +---------------------------+ | - | | | Primary User's PSP App | | - | | +-----------+---------------+ | - | | 13a. PSP sends approval | - | | request to primary user | - | | | | - | | [Primary user approves or | - | | rejects in their PSP app] | - | | 13b. Primary user approves | - | | +----------------->| - ══════════════════════════════════════════════════════════════════════ - | | | | - | | | 14. NPCI debits | - | | | primary user's | - | | | bank (Remitter)| - | | |<--------------->| - | | | 15. NPCI credits| - | | | merchant bank | - | | | (Beneficiary) | - | | |<--------------->| - | | | | - | | 16. Webhook: | | - | | payment.captured | | - | |<-----------------------| | - | | | | - | | 17. Verify signature | | - | | Verify order_id | | - | | Capture payment | | - | | Mark checkout | | - | | completed | | - | | | | - | 18. GET /checkout | | | - | (poll) | | | - |---------------------->| | | - | 19. status: completed | | | - |<----------------------| | | ++------------+ +---------------------------+ +------------------+ +------------+ +| Platform | | Razorpay AI Commerce TSP | | Business | | Razorpay | ++-----+------+ +-------------+-------------+ +--------+---------+ +------+-----+ + | | | | + ════╪════════════════════════════╪════════════════════════════╪══════════════════════╪════ + PRE-CONDITION: Delegation setup completed out-of-band. + 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. POST /delegate/ | | | + | {delegate_id}/ | | | + | token_transactional_ | | | + | data | | | + |--------------------------->| | | + | 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 delegated| + | | | debit (delegate_ | + | | | id + cryptogram) | + | | |--------------------->| + | | | | + | | | [Razorpay resolves | + | | | UMN from | + | | | delegate_id] | + | | | | + | | ══════════╪══════════════════════╪═══ + | | NPCI routes delegated debit: + | | Razorpay → NPCI → Remitter Bank + | | (Primary account holder's bank) + | | No MPIN required. + | | ══════════╪══════════════════════╪═══ + | | | | + | | | 10. Webhook: | + | | | payment.authorized | + | | |<---------------------| + | | | | + | | | 11. Verify signature | + | | | Update checkout | + | | | completed | + | | | | + | 12. status: completed | | | + |<--------------------------------------------------------------| | + | (or complete_in_ | | | + | progress → poll) | | | ``` --- @@ -279,45 +243,38 @@ involved in this step. Before advertising this handler, businesses **MUST** complete: -1. **Create a Razorpay account** at - [dashboard.razorpay.com](https://dashboard.razorpay.com). +1. **Create a Razorpay account** at [dashboard.razorpay.com](https://dashboard.razorpay.com). 2. **Complete KYC** to activate live payments. -3. **Retrieve API keys** from *Settings → API Keys*: - * `key_id` — Public key (`rzp_live_*` or `rzp_test_*`). - * `key_secret` — Private key. **MUST remain on the business backend.** - **MUST NOT appear in any UCP profile, response, or client-side code.** -4. **Enable UPI** as an accepted payment method in the Razorpay dashboard - (*Settings → Payment Methods → UPI*). -5. **Configure webhooks** at *Settings → Webhooks* for the - `payment.captured` event (and optionally `payment.failed`). - -> **Note on NPCI Limits:** The business **MUST NOT** advertise this handler -> for checkout totals exceeding ₹5,000 (500,000 paise). Razorpay will reject -> the payment at the NPCI level if the amount exceeds the per-transaction limit. +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. + * `business_id` (`acc_*`) — the Razorpay account MID for handler config. + +> **Note:** Unlike UPI Intent, no merchant VPA is needed in the handler config. +> The payee VPA is resolved by Razorpay PSP at payment time from the +> `delegate_id` and cryptogram. ### Handler Configuration #### Handler Schema -**Schema URL:** `https://razorpay.com/ucp/handlers/upi-circle/schema.json` +**Schema URL:** `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/config.json` -| Config Variant | Context | Key Fields | -| :---------------- | :------------------- | :----------------------------------------------------- | -| `business_config` | Business discovery | `key_id`, `environment`, `merchant_name`, `max_amount` | -| `platform_config` | Platform discovery | `environment`, `vpa_input`, `poll_interval_ms` | -| `response_config` | Checkout responses | `environment`, `merchant_name`, `per_txn_limit_paise` | +| Config Variant | Context | Key Fields | +| :---------------- | :------------------- | :-------------------------- | +| `business_config` | Business discovery | `business_id`, `environment`| +| `platform_config` | Platform discovery | `environment` | +| `response_config` | Checkout responses | `environment` | #### Business Config Fields -| Field | Type | Required | Description | -| :-------------- | :------ | :------- | :--------------------------------------------------------------------- | -| `environment` | string | Yes | `sandbox` or `production` | -| `key_id` | string | Yes | Public Razorpay API key | -| `merchant_name` | string | No | Display name shown in the buyer's PSP app during authorization | -| `currency` | string | No | Must be `INR`. Defaults to `INR`. | -| `max_amount` | integer | No | Max accepted amount in paise. Cannot exceed 500000. Defaults to 500000.| +| Field | Type | Required | Description | +| :------------ | :----- | :------- | :------------------------------------------------------- | +| `business_id` | string | Yes | Razorpay account identifier (`acc_*`) | +| `environment` | string | Yes | `test` or `production` | -#### Example Business Handler Declaration +#### Example Handler Declaration ```json { @@ -326,18 +283,16 @@ Before advertising this handler, businesses **MUST** complete: "payment_handlers": { "com.razorpay.upi.circle": [ { - "id": "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" } + "id": "razorpay_upi_circle_primary", + "version": "2026-03-26", + "spec": "https://razorpay.com/ucp/upi_circle/2026-03-26/", + "config_schema": "https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/config.json", + "instrument_schemas": [ + "https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/upi_circle_instrument.json" ], "config": { - "environment": "production", - "key_id": "rzp_live_XXXXXXXXXXXXXXX", - "merchant_name": "Acme Store", - "max_amount": 500000 + "business_id": "acc_KN2V9oR1g9n0qH", + "environment": "production" } } ] @@ -348,483 +303,378 @@ Before advertising this handler, businesses **MUST** complete: ### Processing Payments -When the business receives a Complete Checkout request with a `upi_circle` -instrument and `upi_circle` credential containing a `delegate_vpa`, it **MUST**: +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 and `instrument.type == "upi_circle"`. - -2. **Validate VPA format.** Verify `credential.delegate_vpa` matches the - pattern `[a-zA-Z0-9._-+]+@[a-zA-Z0-9]+`. Reject malformed VPAs early. - -3. **Validate amount.** Confirm the checkout total does not exceed ₹5,000 - (500,000 paise). Return `payment_failed` immediately if it does. - -4. **Ensure idempotency.** If this `checkout_id` already has a Razorpay - order and payment, return the existing escalation response. - -5. **Create a Razorpay Order** (`POST /v1/orders`): - - ```http - POST https://api.razorpay.com/v1/orders - Authorization: Basic base64(key_id:key_secret) - Content-Type: application/json - - { - "amount": 50000, - "currency": "INR", - "receipt": "checkout_abc123", - "payment_capture": 0 - } - ``` - - | Field | Description | - | :---------------- | :----------------------------------------------------------- | - | `amount` | Amount in **paise** (1 INR = 100 paise). Max 500000. | - | `currency` | Must be `"INR"` — UPI only supports INR. | - | `receipt` | The UCP `checkout.id` or internal order reference. | - | `payment_capture` | `0` = manual capture (business captures after webhook). | - - Response: `{ "id": "order_Abcdef1234XXXX", ... }` - -6. **Initiate UPI Collect via Razorpay** (`POST /v1/payments/create/upi`): - - ```http - POST https://api.razorpay.com/v1/payments/create/upi - Authorization: Basic base64(key_id:key_secret) - Content-Type: application/json - - { - "amount": 50000, - "currency": "INR", - "order_id": "order_Abcdef1234XXXX", - "method": "upi", - "vpa": "buyer@paytm", - "email": "buyer@example.com", - "contact": "+919876543210", - "description": "Order checkout_abc123", - "callback_url": "https://business.example.com/razorpay/callback" - } - ``` - - | Field | Description | - | :------------- | :---------------------------------------------------------------- | - | `method` | Must be `"upi"`. | - | `vpa` | The delegate VPA from `credential.delegate_vpa`. | - | `callback_url` | Razorpay posts payment status here (used alongside webhooks). | - - Response: `{ "razorpay_payment_id": "pay_XXXXXXXXXXXXXXXX", "next": [...] }` - - > Razorpay sends a UPI collect request to NPCI, which routes it to the - > buyer's PSP app. The PSP recognizes the VPA as belonging to a UPI Circle - > delegation and presents the appropriate authorization UI. - -7. **Return `requires_escalation`** with `requires_buyer_authentication`: - - ```json - { - "status": "requires_escalation", - "continue_url": "https://business.example.com/checkout-sessions/checkout_abc123", - "messages": [ - { - "type": "error", - "code": "requires_buyer_authentication", - "severity": "requires_buyer_review", - "content": "Open your UPI app to authorize this payment", - "path": "$.payment.instruments[0]" - } - ], - "payment": { - "instruments": [ - { - "id": "instr_1", - "handler_id": "razorpay_upi_circle", - "type": "upi_circle", - "selected": true, - "display": { - "name": "Pay via UPI Circle", - "logo": "https://upload.wikimedia.org/wikipedia/commons/f/fa/UPI-Logo.png" - }, - "credential": { - "type": "upi_circle", - "delegate_vpa": "buyer@paytm", - "razorpay_payment_id": "pay_XXXXXXXXXXXXXXXX", - "push_sent_at": "2026-03-31T10:00:00Z", - "expires_at": "2026-03-31T10:15:00Z" - } - } - ] - } - } - ``` - -8. **Receive Razorpay webhook.** On `payment.captured`: - - Verify `X-Razorpay-Signature` using `HMAC_SHA256(webhook_body, webhook_secret)`. - - Confirm `payment.order_id` matches the order created for this checkout. - - Capture the payment (`POST /v1/payments/{id}/capture`) if not auto-captured. - - Mark the UCP checkout as `completed`. - -#### Handling Partial Delegation Timeout - -For partial delegation, the primary account holder must approve the payment. -This can take several minutes. The business **MUST** configure the webhook -for `payment.failed` as well and handle timeout scenarios: - -- If `payment.failed` is received with `description: "Payment was cancelled by the user"`, - mark the checkout as `requires_escalation` again (allowing retry) or `canceled`. -- The escalation `expires_at` should be set to 5–15 minutes to cover approval time. - -#### Error Mapping - -| Condition | UCP Error | Action | -| :------------------------------------------------- | :--------------- | :----------------------------------------------- | -| Checkout total exceeds ₹5,000 | `payment_failed` | Reject at handler selection — do not call Razorpay | -| Invalid or unresolvable delegate VPA | `payment_failed` | Return error; prompt buyer to re-enter VPA | -| Order/payment creation fails | `payment_failed` | Return error; platform may retry | -| Webhook `payment.failed` — buyer rejected | `payment_failed` | Retry or `canceled` | -| Webhook `payment.failed` — primary user rejected | `payment_failed` | Return error with partial delegation context | -| Authorization timed out (>15 min) | `payment_failed` | Business must create a new order | -| VPA not enrolled in UPI Circle | `payment_failed` | Return error; buyer must set up circle first | -| Currency is not `INR` | `payment_failed` | Reject at handler selection stage | + `com.razorpay.upi.circle` handler. ---- +2. **Ensure idempotency.** If this `checkout_id` has already been processed, + return the previous result without re-processing. -## Platform Integration +3. **Extract credential.** Retrieve `credential.cryptogram` and + `credential.delegate_id` from the instrument. -### Prerequisites +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`). -Platforms do **not** need a Razorpay account. The platform only needs to: +5. **Return result.** -1. Render a VPA input field to collect the buyer's delegate UPI ID. -2. Handle the `requires_buyer_authentication` error code in the - `requires_escalation` response. -3. Display a "pending authorization" screen instructing the buyer to open - their UPI app (no deep link needed — the push notification is automatic). -4. Poll for checkout completion. +**Success Response:** -### Handler Configuration +```json +{ + "id": "checkout_abc123", + "status": "completed", + "order": { + "id": "order_xyz789", + "permalink_url": "https://store.com/orders/xyz789" + } +} +``` -#### Example Platform Handler Declaration +**Failure Response:** ```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", - "vpa_input": { - "placeholder": "Enter UPI Circle ID (e.g. name@upi)", - "label": "UPI Circle ID" - }, - "poll_interval_ms": 3000, - "poll_timeout_ms": 300000 - } - } - ] + "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" } - } + ] } ``` -### Payment Protocol +### Webhook Handling -#### Step 1: Discover Handler +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) +``` -The platform identifies `com.razorpay.upi.circle` in the business's profile -and confirms `upi_circle` in `available_instruments`. +| 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 | -Check that the checkout total does not exceed `response_config.per_txn_limit_paise` -(₹5,000). If it does, do not offer UPI Circle as a payment option. +--- + +## Platform Integration + +### Prerequisites + +Before handling `com.razorpay.upi.circle` payments, platforms **MUST**: -#### Step 2: Create / Fetch Checkout +1. **Complete delegation setup** — Buyer must have an active delegation mandate + via Razorpay AI Commerce TSP. Platform must hold a valid `delegate_id` with + `status: linked`. +2. **Integrate with Razorpay AI Commerce TSP** — Ability to call + `POST /delegate/{delegate_id}/token_transactional_data` to fetch cryptograms. +3. **Implement status polling** — Poll `GET /checkout-sessions/{id}` if the + business processes asynchronously and returns `status: complete_in_progress`. -The platform creates or fetches the checkout session. The business returns -`response_config` with `environment`, `merchant_name`, and NPCI limits. +Platforms **SHOULD** only present UPI Circle when: +- The buyer's context indicates India (e.g., `address_country: "IN"`) +- The buyer has an active delegation (`delegate_id` with `status: linked`) + +### 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": "checkout_abc123", - "status": "incomplete", - "currency": "INR", - "totals": [ - { "type": "subtotal", "amount": 49000 }, - { "type": "tax", "amount": 1000 }, - { "type": "total", "amount": 50000 } - ], - "ucp": { - "payment_handlers": { - "com.razorpay.upi.circle": [ - { - "id": "razorpay_upi_circle", - "version": "{{ ucp_version }}", - "available_instruments": [{ "type": "upi_circle" }], - "config": { - "environment": "production", - "merchant_name": "Acme Store", - "per_txn_limit_paise": 500000, - "monthly_limit_paise": 1500000 - } - } - ] - } + "id": "razorpay_upi_circle_primary", + "name": "com.razorpay.upi.circle", + "config": { + "business_id": "acc_KN2V9oR1g9n0qH", + "environment": "production" } } ``` -#### Step 3: Collect Delegate VPA from Buyer +#### Step 2: Fetch Cryptogram from TSP -The platform renders a VPA input. The buyer enters their delegate UPI ID -(the UPI ID they have registered as a secondary user in a UPI Circle). +Call Razorpay AI Commerce TSP 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/ai-tpap/delegate/{delegate_id}/token_transactional_data +Authorization: Basic base64(key_id:key_secret) +Content-Type: application/json + +{ + "delegate_id": "c894aa29a7da69" +} ``` -┌─────────────────────────────────────────────────────┐ -│ Pay via UPI Circle │ -│ │ -│ UPI Circle ID │ -│ ┌───────────────────────────────────────────────┐ │ -│ │ Enter UPI Circle ID (e.g. name@upi) │ │ -│ └───────────────────────────────────────────────┘ │ -│ │ -│ ℹ️ Limit: ₹5,000 per transaction · ₹15,000/month │ -│ │ -│ [Pay ₹500.00] │ -└─────────────────────────────────────────────────────┘ + +**Response:** + +```json +{ + "delegate_id": "c894aa29a7da69", + "cryptogram_value": "a345345dfgdfasdfh45jtyhgjkyutsdasd2", + "expired_at": 1748716199 +} ``` -#### Step 4: Submit Complete Checkout (with VPA credential) +> 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 + +```json +{ + "id": "pi_001", + "handler_id": "razorpay_upi_circle_primary", + "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" + } +} +``` -The platform submits Complete Checkout with the `upi_circle` instrument -**and** the delegate VPA as the credential. Unlike UPI Intent, the credential -is provided by the buyer, not generated by the business. +#### Step 4: Submit Complete Checkout ```http POST /checkout-sessions/{checkout_id}/complete -UCP-Agent: profile="https://platform.example/profile" Content-Type: application/json +UCP-Agent: profile="https://agent.example/profile" { "payment": { "instruments": [ { - "id": "instr_1", - "handler_id": "razorpay_upi_circle", + "id": "pi_001", + "handler_id": "razorpay_upi_circle_primary", "type": "upi_circle", + "delegate_id": "c894aa29a7da69", + "selected": true, "display": { - "name": "Pay via UPI Circle" + "name": "UPI Autopay", + "masked_vpa": "roh***@icici" }, "credential": { - "type": "upi_circle", - "delegate_vpa": "buyer@paytm" + "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 Escalation Response +#### Step 5: Handle Response -The business responds with `requires_escalation` and -`requires_buyer_authentication`. Unlike UPI Intent, there is **no deep link -to open** — the push notification was already sent to the buyer's PSP app. +**Synchronous success** — Business responds directly with `completed`: -```javascript -const response = await completeCheckout(checkoutId, instrument, credential); - -if (response.status === "requires_escalation") { - const authError = response.messages.find( - m => m.code === "requires_buyer_authentication" && - m.severity === "requires_buyer_review" - ); - const instrument = response.payment?.instruments?.[0]; - const cred = instrument?.credential; - - if (authError && cred?.type === "upi_circle") { - // Check expiry before showing pending screen - if (cred.expires_at && new Date(cred.expires_at) < new Date()) { - fallbackToContinueUrl(response.continue_url); - return; - } - - // Show "check your UPI app" screen (no deep link needed) - showUpiCirclePendingScreen({ - vpa: cred.delegate_vpa, - expiresAt: cred.expires_at, - merchantName: handlerConfig.merchant_name, - }); - - // Start polling — allow up to 5 minutes for partial delegation approval - pollCheckoutStatus(checkoutId, response.continue_url); - } else { - fallbackToContinueUrl(response.continue_url); +```json +{ + "id": "checkout_abc123", + "status": "completed", + "order": { + "id": "order_xyz789", + "permalink_url": "https://store.com/orders/xyz789" } } ``` -#### Step 6: Poll for Completion - -After displaying the pending screen, the platform polls until `completed`, -`canceled`, or timeout: +**Async processing** — Business returns `complete_in_progress`. Platform polls: ```javascript -async function pollCheckoutStatus(checkoutId, continueUrl) { - const POLL_INTERVAL_MS = 3000; - const MAX_POLLS = 100; // ~5 minutes (covers partial delegation approval) - - for (let i = 0; i < MAX_POLLS; i++) { - await sleep(POLL_INTERVAL_MS); +async function pollCheckout(checkoutId) { + for (let i = 0; i < 60; i++) { + await sleep(2000); const checkout = await getCheckout(checkoutId); if (checkout.status === "completed") { - showSuccessScreen(checkout); - return; + return { success: true, order: checkout.order }; } if (checkout.status === "canceled") { - showFailureScreen("Payment was declined or timed out."); - return; + return { success: false, messages: checkout.messages }; } } - - // Timeout — fall back to continue_url - fallbackToContinueUrl(continueUrl); + return { success: false, reason: "timeout" }; } ``` -> **Partial delegation timeout:** Allow up to 5 minutes of polling -> (`poll_timeout_ms: 300000`). The primary account holder may need time -> to see and approve the notification on their device. +**Failure** — Platform reads `messages`. For `recoverable` errors, the platform +MAY retry with a fresh cryptogram. For `requires_buyer_input` errors (limit +exceeded, mandate expired), surface a message to the buyer. --- -## Security Considerations +## PSP Integration (Razorpay) -| Requirement | Description | -| :-------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| **key_secret never exposed** | The `key_secret` **MUST** remain on the business backend. **MUST NOT** appear in UCP profiles, responses, or client-side code. | -| **Webhook signature** | The business **MUST** verify `X-Razorpay-Signature` on every webhook using `HMAC_SHA256(body, webhook_secret)` before updating checkout state.| -| **Order-to-checkout binding** | The business **MUST** verify that `payment.order_id` in the webhook matches the order created for this checkout. | -| **Amount enforcement** | The business **MUST** reject any checkout exceeding ₹5,000 (500,000 paise) — the NPCI per-transaction limit for UPI Circle. | -| **VPA validation** | The business **MUST** validate `delegate_vpa` format before calling Razorpay. Invalid VPAs will cause Razorpay API errors. | -| **Idempotency** | If a `checkout_id` already has a payment order, return the existing escalation response without creating a new order. | -| **Credential expiry** | The platform **MUST NOT** display an expired pending screen. It **MUST** fall back to `continue_url` if `expires_at` has passed. | -| **continue_url fallback** | The platform **MUST** fall back to `continue_url` if: the handler is not implemented, the credential is expired, or polling times out. | -| **No primary user data exposure** | The business response **MUST NOT** include any information about the primary account holder. Only the delegate VPA and payment reference are safe to return. | -| **INR only** | UPI only supports INR. Businesses **MUST NOT** advertise this handler for non-INR checkouts. | -| **TLS/HTTPS only** | All traffic to Razorpay APIs and UCP endpoints **MUST** use TLS 1.2 or higher. | +### Delegated Debit Processing ---- +When the business submits a `upi_circle` instrument with a +`upi_circle_cryptogram` credential, Razorpay PSP: -## Testing +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 provides a full sandbox environment for end-to-end testing. +### Razorpay AI Commerce TSP: Cryptogram Issuance -### Setup +The TSP acts as the credential provider — analogous to Google Pay's token API +or Stripe's Shared Payment Token (SPT) API: -1. Toggle to **Test Mode** in the [Razorpay Dashboard](https://dashboard.razorpay.com). -2. From *Settings → API Keys*, generate test keys (`rzp_test_*`). -3. Use `environment: "sandbox"` in both business and platform configs. +| API | Method | Purpose | +| :-------------------------------------------------------- | :----- | :----------------------------------- | +| `POST /delegate/{delegate_id}/token_transactional_data` | POST | Issue a one-time cryptogram | -### Simulating UPI Circle in Sandbox +**Response:** -In sandbox mode, use Razorpay test VPAs to simulate different scenarios: +```json +{ + "delegate_id": "c894aa29a7da69", + "cryptogram_value": "a345345dfgdfasdfh45jtyhgjkyutsdasd2", + "expired_at": 1748716199 +} +``` -| Test VPA | Simulated Scenario | -| :-------------------- | :---------------------------------------- | -| `success@razorpay` | Payment authorized (full delegation) | -| `failure@razorpay` | Payment declined by secondary user | -| `partial@razorpay` | Partial delegation — primary approves | -| `timeout@razorpay` | Authorization times out after 15 seconds | +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). -> Contact Razorpay support for the current list of sandbox UPI Circle test VPAs, -> as these may be updated with new sandbox releases. +--- -### End-to-End Test Checklist +## Error Handling -- [ ] Business profile at `/.well-known/ucp` declares `com.razorpay.upi.circle` with `rzp_test_*` key and `environment: "sandbox"`. -- [ ] Platform profile declares `com.razorpay.upi.circle` with `environment: "sandbox"`. -- [ ] Platform does **not** offer UPI Circle when checkout total exceeds ₹5,000. -- [ ] Platform renders VPA input field with correct label and placeholder from `response_config`. -- [ ] Platform submits Complete Checkout with `upi_circle` instrument **and** `upi_circle` credential containing `delegate_vpa`. -- [ ] Business validates VPA format before calling Razorpay. -- [ ] Business creates Razorpay order via `POST /v1/orders` in test mode. -- [ ] Business initiates collect via `POST /v1/payments/create/upi` with the delegate VPA. -- [ ] Business returns `requires_escalation` with `requires_buyer_authentication` + `upi_circle_credential`. -- [ ] Escalation response includes `continue_url` (always required). -- [ ] `credential.razorpay_payment_id` is present and non-empty. -- [ ] `credential.expires_at` is in the future. -- [ ] Platform shows "check your UPI app" screen — does NOT open a deep link. -- [ ] Platform polls with 3-second interval, up to 5-minute timeout. -- [ ] Business receives `payment.captured` webhook; verifies `X-Razorpay-Signature`. -- [ ] Business marks checkout `completed`. -- [ ] Platform polling returns `status: completed`. -- [ ] Platform falls back to `continue_url` when credential is expired. -- [ ] Platform falls back to `continue_url` when polling times out. -- [ ] `payment.failed` webhook correctly triggers checkout cancellation. - -### Webhook Signature Verification Reference +| NPCI / Mandate Error | UCP `code` | `severity` | Platform Action | +| :------------------------------------ | :----------------- | :-------------------- | :---------------------------------------------------- | +| Mandate limit exceeded (per-txn) | `payment_declined` | `requires_buyer_input`| Lower amount or suggest UPI Intent | +| Mandate limit exceeded (monthly) | `payment_declined` | `requires_buyer_input`| Wait for limit reset or suggest UPI Intent | +| 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 | -```python -import hmac -import hashlib +**Error Response Example:** -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) +```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" + } + ] +} ``` --- -## Comparison with UPI Intent Handler - -| Aspect | `com.razorpay.upi` (Intent) | `com.razorpay.upi.circle` (Circle) | -| :---------------------------- | :---------------------------------------------- | :----------------------------------------------------- | -| **Instrument type** | `upi_intent` | `upi_circle` | -| **Platform submits credential?** | No — no credential on submit | Yes — delegate VPA | -| **Business generates credential?** | Yes — `intent_uri` in escalation response | No — returns payment reference only | -| **Platform action post-escalation** | Open `upi://` deep link or render QR | Show "check your app" screen, start polling | -| **Buyer auth in PSP app** | MPIN | Biometrics / app PIN (no MPIN) | -| **Additional approver** | None | Primary user (partial delegation only) | -| **Per-txn limit** | No special limit | ₹5,000 (NPCI) | -| **Monthly limit** | No special limit | ₹15,000 per circle link (NPCI) | -| **Recommended for** | Standard UPI payments by any buyer | Secondary users paying via delegated access | -| **Poll timeout** | ~1 minute | ~5 minutes (partial delegation needs more time) | +## 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+. | --- -## Applicability Beyond Razorpay +## Testing + +### End-to-End Test Checklist -The `requires_buyer_authentication` error code and `upi_circle` credential -pattern are PSP-agnostic. Any UPI PSP (PayU, Cashfree, Juspay) that supports -NPCI UPI Circle delegated payments can implement this same handler by -accepting the `delegate_vpa` credential and processing the collect request -through the NPCI network. +- [ ] 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 `business_id` and `environment`. +- [ ] Platform completes delegation setup and holds a `delegate_id` with `status: linked`. +- [ ] Platform calls TSP 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 -* **Handler Spec:** `https://razorpay.com/ucp/handlers/upi-circle` -* **Handler Schema:** `https://razorpay.com/ucp/handlers/upi-circle/schema.json` -* **UPI Circle Instrument Schema:** [upi_circle_instrument.json](site:schemas/shopping/types/upi_circle_instrument.json) -* **UPI Circle Credential Schema:** [upi_circle_credential.json](site:schemas/shopping/types/upi_circle_credential.json) -* **Razorpay Orders API:** `https://razorpay.com/docs/api/orders/` -* **Razorpay Payments API:** `https://razorpay.com/docs/api/payments/` -* **Razorpay UPI Docs:** `https://razorpay.com/docs/payments/payment-methods/upi/` -* **NPCI UPI Circle Overview:** `https://www.npci.org.in/what-we-do/upi/product-overview` -* **UPI Intent Handler:** [razorpay-upi-payment-handler.md](razorpay-upi-payment-handler.md) -* **UCP Payment Handler Guide:** [payment-handler-guide.md](payment-handler-guide.md) -* **UCP Checkout Specification:** [checkout.md](checkout.md) +| Resource | URL | +| :------------------------------- | :------------------------------------------------------------------------------------- | +| Handler Spec | `https://razorpay.com/ucp/upi_circle/2026-03-26/` | +| Config Schema | `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/config.json` | +| Instrument Schema | `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/upi_circle_instrument.json` | +| Credential Schema | `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/upi_circle_credential.json` | +| Razorpay AI Commerce TSP API | `https://razorpay.com/docs/ucp/ai-tpap/` | +| 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) | +| UPI Intent Handler (comparison) | [razorpay-upi-payment-handler.md](razorpay-upi-payment-handler.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/types/business_config.json b/source/handlers/razorpay-upi-circle/types/business_config.json index beb6bb98b..e46c8a012 100644 --- a/source/handlers/razorpay-upi-circle/types/business_config.json +++ b/source/handlers/razorpay-upi-circle/types/business_config.json @@ -2,35 +2,18 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/business_config.json", "title": "Razorpay UPI Circle Business Config", - "description": "Business-level configuration for the com.razorpay.upi.circle payment handler. Declared in the business's UCP profile at /.well-known/ucp. UPI Circle enables secondary users to pay from a primary account holder's bank account within NPCI-mandated delegation limits (₹5,000 per transaction, ₹15,000 per month).", + "description": "Business-level configuration for the com.razorpay.upi.circle payment handler. Declared in the business's UCP profile. Unlike UPI Intent, no merchant VPA is required here — payee VPA is resolved by Razorpay PSP at payment processing time using the delegate_id and cryptogram.", "type": "object", - "required": ["environment", "key_id"], + "required": ["business_id", "environment"], "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": { + "business_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]+$" + "description": "Razorpay account identifier (MID) for the business. Used by Razorpay PSP to route the delegated debit to the correct merchant." }, - "merchant_name": { - "type": "string", - "description": "Business display name shown to the buyer during the UPI Circle payment authorization in the PSP app." - }, - "currency": { + "environment": { "type": "string", - "description": "Currency accepted by this handler. Defaults to 'INR' — UPI only supports INR transactions.", - "default": "INR" - }, - "max_amount": { - "type": "integer", - "description": "Maximum transaction amount in paise that this handler will accept. Must not exceed the NPCI UPI Circle per-transaction limit of ₹5,000 (500000 paise). Defaults to 500000 if not specified.", - "maximum": 500000, - "default": 500000 + "enum": ["test", "production"], + "description": "Razorpay API environment. Use 'test' for sandbox testing, 'production' for live payments." } - }, - "additionalProperties": false + } } diff --git a/source/handlers/razorpay-upi-circle/types/platform_config.json b/source/handlers/razorpay-upi-circle/types/platform_config.json index 23c7f9004..296738371 100644 --- a/source/handlers/razorpay-upi-circle/types/platform_config.json +++ b/source/handlers/razorpay-upi-circle/types/platform_config.json @@ -2,42 +2,14 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/platform_config.json", "title": "Razorpay UPI Circle Platform Config", - "description": "Platform-level configuration for the com.razorpay.upi.circle payment handler. Declared in the platform's UCP profile. Unlike UPI Intent (which requires deep-link capability), UPI Circle requires the platform to collect the buyer's delegate VPA and display a pending authorization screen while the buyer authenticates in their PSP app.", + "description": "Platform-level configuration for the com.razorpay.upi.circle payment handler. Unlike UPI Intent (which requires deep-link capability), UPI Circle requires the platform to hold a valid delegate_id and integrate with Razorpay AI Commerce TSP to fetch one-time cryptograms before each Complete Checkout.", "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)." - }, - "vpa_input": { - "type": "object", - "description": "Configuration for the delegate VPA input field shown to the buyer.", - "properties": { - "placeholder": { - "type": "string", - "description": "Placeholder text for the VPA input (e.g., 'Enter your UPI ID'). Defaults to 'Enter UPI Circle ID (e.g. name@upi)'." - }, - "label": { - "type": "string", - "description": "Label for the VPA input field (e.g., 'UPI Circle ID'). Defaults to 'UPI Circle ID'." - } - }, - "additionalProperties": false - }, - "poll_interval_ms": { - "type": "integer", - "description": "How often (in milliseconds) the platform polls for checkout completion after submitting the VPA. Defaults to 3000 (3 seconds). Minimum 2000.", - "minimum": 2000, - "default": 3000 - }, - "poll_timeout_ms": { - "type": "integer", - "description": "Maximum time (in milliseconds) the platform will poll before falling back to continue_url. Defaults to 300000 (5 minutes) to account for partial delegation approval time.", - "minimum": 30000, - "default": 300000 + "enum": ["test", "production"], + "description": "Razorpay API environment. Must match the business config environment." } - }, - "additionalProperties": false + } } diff --git a/source/handlers/razorpay-upi-circle/types/response_config.json b/source/handlers/razorpay-upi-circle/types/response_config.json index 5580f2ef0..c9345b952 100644 --- a/source/handlers/razorpay-upi-circle/types/response_config.json +++ b/source/handlers/razorpay-upi-circle/types/response_config.json @@ -2,29 +2,14 @@ "$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. After the platform submits Complete Checkout with a upi_circle instrument and the buyer's delegate VPA credential, the business submits a collect request to Razorpay and returns requires_escalation. This config provides display context and NPCI limit information the platform needs to render the pending authorization screen.", + "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": ["sandbox", "production"], + "enum": ["test", "production"], "description": "Razorpay API environment for this checkout." - }, - "merchant_name": { - "type": "string", - "description": "Business display name shown to the buyer during UPI Circle authorization in the PSP app." - }, - "per_txn_limit_paise": { - "type": "integer", - "description": "Per-transaction limit in paise for UPI Circle payments. NPCI mandates a maximum of ₹5,000 (500000 paise). Informational — the platform may display this to the buyer.", - "default": 500000 - }, - "monthly_limit_paise": { - "type": "integer", - "description": "Monthly cumulative limit in paise for UPI Circle payments. NPCI mandates a maximum of ₹15,000 (1500000 paise). Informational — the platform may display this to the buyer.", - "default": 1500000 } - }, - "additionalProperties": false + } } diff --git a/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json index 85d9a8744..9b34714a9 100644 --- a/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json +++ b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json @@ -2,12 +2,12 @@ "$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. NPCI limits apply: ₹5,000 per transaction, ₹15,000 per month per UPI Circle link.", + "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 secondary user must have an active UPI Circle delegation from a primary account holder via their PSP.", + "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 AI Commerce TSP delegation setup.", "allOf": [ { "$ref": "https://ucp.dev/schemas/shopping/types/available_payment_instrument.json" } ], diff --git a/source/schemas/shopping/types/upi_circle_credential.json b/source/schemas/shopping/types/upi_circle_credential.json index 273398574..61a2a166c 100644 --- a/source/schemas/shopping/types/upi_circle_credential.json +++ b/source/schemas/shopping/types/upi_circle_credential.json @@ -2,44 +2,32 @@ "$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": "Credential for UPI Circle (delegated) payments. Used in two directions: (1) the platform submits the secondary user's delegate VPA to the business during Complete Checkout; (2) the business returns a payment reference in the escalation response after submitting the collect request to the PSP. The delegate_vpa is required on input; razorpay_payment_id and push_sent_at are populated by the business on output.", + "description": "One-time cryptogram for a delegated UPI payment. Fetched by the platform from the PSP's AI Commerce TSP 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", "delegate_vpa"], + "required": ["type", "cryptogram"], "properties": { "type": { - "const": "upi_circle", - "description": "Discriminator for UPI Circle credential." + "const": "upi_circle_cryptogram", + "description": "Discriminator for UPI Circle cryptogram credential." }, - "delegate_vpa": { + "cryptogram": { "type": "string", - "pattern": "^[a-zA-Z0-9.\\-_+]+@[a-zA-Z0-9]+$", - "minLength": 3, - "maxLength": 255, - "description": "The secondary (delegate) user's UPI Virtual Payment Address (VPA/UPI ID). This is the UPI ID of the buyer making the payment — it resolves to the primary account holder's bank account via the UPI Circle delegation. Example: 'buyer@paytm'." + "minLength": 1, + "description": "One-time cryptogram value issued by the PSP's AI Commerce TSP. Single-use and time-limited — authorizes exactly one delegated debit. NPCI rejects reuse. The platform MUST fetch a fresh cryptogram for each transaction." }, - "delegation_type": { + "delegate_id": { "type": "string", - "enum": ["full", "partial"], - "description": "UPI Circle delegation type. 'full' means the secondary user can authorize payments independently (up to NPCI limits). 'partial' means each transaction also requires explicit approval from the primary account holder. If absent, the PSP determines the delegation type from the VPA's circle configuration." - }, - "razorpay_payment_id": { - "type": "string", - "description": "Razorpay payment identifier (e.g., 'pay_XXXXXXXXXXXXXXXX'). Populated by the business in the escalation response after the PSP collect request is initiated. Absent when submitted by the platform." - }, - "push_sent_at": { - "type": "string", - "format": "date-time", - "description": "RFC 3339 timestamp indicating when the push notification was dispatched to the secondary user's PSP app. Populated by the business in the escalation response." + "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 for this payment authorization window. Per NPCI best practice, UPI collect requests expire after 5–15 minutes. The platform MUST display a timeout indicator and fall back to continue_url if the credential expires before polling returns completed." + "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 index f1d279df1..201e48a9a 100644 --- a/source/schemas/shopping/types/upi_circle_instrument.json +++ b/source/schemas/shopping/types/upi_circle_instrument.json @@ -2,18 +2,23 @@ "$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 UPI Circle (delegated) payments. Extends the base payment instrument for secondary users who pay via a UPI Circle delegation from a primary account holder's bank account. Unlike UPI Intent (where no credential is submitted and an intent URI is returned), UPI Circle requires the secondary user's delegate VPA as an input credential. The buyer authenticates via biometrics or PSP app PIN — no bank MPIN is required from the secondary user.", + "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 the PSP's AI Commerce TSP 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"], + "required": ["type", "delegate_id"], "properties": { "type": { "const": "upi_circle", - "description": "Discriminator for UPI Circle instrument." + "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 the PSP's AI Commerce TSP. 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", @@ -22,17 +27,21 @@ "name": { "type": "string", "maxLength": 100, - "description": "Human-readable name for platform UI (e.g., 'Pay via UPI Circle')." + "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." }, - "description": { + "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", - "maxLength": 255, - "description": "Optional description shown to the buyer (e.g., 'Pay using your linked UPI Circle account')." + "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." } } } From 3db838561231fe64831e2b54eefc15874049a9a4 Mon Sep 17 00:00:00 2001 From: Paras Kathuria Date: Tue, 31 Mar 2026 12:41:02 +0530 Subject: [PATCH 3/6] fix: align naming with official UCP specification terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace invented "Razorpay AI Commerce TSP" naming with official UCP terms: - Participants table: "Razorpay AI Commerce TSP" row renamed to "Razorpay (Credential Provider)" — the official UCP term for an entity that issues payment credentials (analogous to Google Pay token API) - "AI Platform" → "Platform" throughout (official UCP participant name) - Removed invented "ai-tpap" from API paths in delegation setup; replaced with descriptive operation names and a note to refer to Razorpay docs - Removed invented razorpay.com/ucp/upi_circle/2026-03-26/ schema URLs from References and handler declaration example - Schema descriptions: "PSP's AI Commerce TSP" → "Razorpay" All naming now matches ucp.dev/latest/specification/* terminology. --- .../razorpay-upi-circle-payment-handler.md | 98 +++++++++---------- .../types/platform_config.json | 2 +- .../types/upi_circle_instrument.json | 2 +- .../shopping/types/upi_circle_credential.json | 4 +- .../shopping/types/upi_circle_instrument.json | 4 +- 5 files changed, 51 insertions(+), 59 deletions(-) diff --git a/docs/specification/razorpay-upi-circle-payment-handler.md b/docs/specification/razorpay-upi-circle-payment-handler.md index 7fdefa2bd..d7284b3d2 100644 --- a/docs/specification/razorpay-upi-circle-payment-handler.md +++ b/docs/specification/razorpay-upi-circle-payment-handler.md @@ -29,14 +29,14 @@ 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, sends it in Complete Checkout, and the business -processes it straight to `completed`. No escalation, no app switch, no buyer -interaction at payment time. +Razorpay, sends it in Complete Checkout, and the business processes it +straight to `completed`. No escalation, no app switch, no buyer interaction +at payment time. > **Note:** This handler requires a **one-time delegation setup** between the -> buyer, the AI Platform, and Razorpay AI Commerce TSP. This setup happens -> outside UCP as a bilateral integration — analogous to a buyer saving a card -> on ChatGPT. See [Delegation Setup](#delegation-setup-outside-ucp). +> buyer, the Platform, and Razorpay. This setup happens outside UCP as a +> bilateral integration — analogous to a buyer saving a card on ChatGPT. +> See [Delegation Setup](#delegation-setup-outside-ucp). ### UPI Circle vs UPI Intent — Architecture Comparison @@ -75,25 +75,25 @@ interaction at payment time. | Participant | Role | Prerequisites | | :-------------------------- | :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------ | -| **Business** | Advertises handler configuration; processes cryptogram via Razorpay PSP; responds `completed`| Yes — Razorpay account with KYC, webhook endpoint (HTTPS, TLS 1.2+)| -| **Platform** | Discovers handler; fetches cryptogram from TSP; submits in Complete Checkout; polls if async | Yes — Razorpay AI Commerce TSP integration, active `delegate_id` | -| **PSP (Razorpay)** | Processes delegated debit via NPCI using mandate UMN; fires webhook to business | N/A — infrastructure provider | -| **Razorpay AI Commerce TSP**| Issues one-time cryptograms for established delegation mandates | N/A — credential provider (analogous to Google Pay API or Stripe SPT)| +| **Business** | Advertises handler configuration; processes cryptogram via Razorpay; responds `completed` | Yes — Razorpay account with KYC, webhook endpoint (HTTPS, TLS 1.2+) | +| **Platform** | Discovers handler; fetches cryptogram from Razorpay; submits in Complete Checkout | Yes — Razorpay API integration, active `delegate_id` | +| **Razorpay (PSP)** | Processes delegated debit via NPCI using mandate UMN; fires webhook to business | N/A — payment infrastructure | +| **Razorpay (Credential Provider)** | 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 AI Platform and -Razorpay AI Commerce TSP — it is **not part of UCP**, just as saving a card on -ChatGPT is not part of the checkout protocol. +This is a **one-time bilateral integration** between the Platform and Razorpay +— it is **not part of UCP**, just as saving a card on ChatGPT is not part of +the checkout protocol. ### Setup Flow ``` +------------+ +---------------------------+ +-----------+ -| Platform | | Razorpay AI Commerce TSP | | Buyer UPI | +| Platform | | Razorpay | | Buyer UPI | +-----+------+ +-------------+-------------+ | App | | | +-----+-----+ | | | @@ -137,17 +137,17 @@ ChatGPT is not part of the checkout protocol. ### Setup API Reference -| API | Method | Purpose | -| :---------------------------------------------- | :----- | :-------------------------------------------- | -| `/v1/upi/ai-tpap/customers/generate_otp` | POST | Send OTP to buyer's mobile | -| `/v1/upi/ai-tpap/customers/{cust_id}/otp_submit`| POST | Verify OTP; receive intent link / QR for delegation | -| `/v1/upi/ai-tpap/delegate/{delegate_id}` | GET | Poll delegation status until `linked` | +| 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. -> Detailed API specifications are maintained separately. See -> [Razorpay AI Commerce TSP API Docs](https://razorpay.com/docs/ucp/ai-tpap/). +> Exact API paths are published in Razorpay's documentation. Refer to the +> Razorpay developer docs for the current endpoint reference. --- @@ -156,9 +156,9 @@ payments use this ID to fetch cryptograms. ### Flow Diagram ``` -+------------+ +---------------------------+ +------------------+ +------------+ -| Platform | | Razorpay AI Commerce TSP | | Business | | Razorpay | -+-----+------+ +-------------+-------------+ +--------+---------+ +------+-----+ ++------------+ +---------------------------+ +------------------+ +------------------+ +| Platform | | Razorpay (Cred. Provider)| | Business | | Razorpay (PSP) | ++-----+------+ +-------------+-------------+ +--------+---------+ +--------+---------+ | | | | ════╪════════════════════════════╪════════════════════════════╪══════════════════════╪════ PRE-CONDITION: Delegation setup completed out-of-band. @@ -259,8 +259,6 @@ Before advertising this handler, businesses **MUST** complete: #### Handler Schema -**Schema URL:** `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/config.json` - | Config Variant | Context | Key Fields | | :---------------- | :------------------- | :-------------------------- | | `business_config` | Business discovery | `business_id`, `environment`| @@ -284,12 +282,7 @@ Before advertising this handler, businesses **MUST** complete: "com.razorpay.upi.circle": [ { "id": "razorpay_upi_circle_primary", - "version": "2026-03-26", - "spec": "https://razorpay.com/ucp/upi_circle/2026-03-26/", - "config_schema": "https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/config.json", - "instrument_schemas": [ - "https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/upi_circle_instrument.json" - ], + "name": "com.razorpay.upi.circle", "config": { "business_id": "acc_KN2V9oR1g9n0qH", "environment": "production" @@ -384,10 +377,9 @@ def verify_razorpay_webhook(body: bytes, signature: str, webhook_secret: str) -> Before handling `com.razorpay.upi.circle` payments, platforms **MUST**: 1. **Complete delegation setup** — Buyer must have an active delegation mandate - via Razorpay AI Commerce TSP. Platform must hold a valid `delegate_id` with - `status: linked`. -2. **Integrate with Razorpay AI Commerce TSP** — Ability to call - `POST /delegate/{delegate_id}/token_transactional_data` to fetch cryptograms. + via Razorpay. Platform must hold a valid `delegate_id` with `status: linked`. +2. **Integrate with Razorpay** — Ability to call Razorpay's cryptogram issuance + API to fetch a one-time cryptogram per transaction. 3. **Implement status polling** — Poll `GET /checkout-sessions/{id}` if the business processes asynchronously and returns `status: complete_in_progress`. @@ -413,14 +405,14 @@ from the Create Checkout response. } ``` -#### Step 2: Fetch Cryptogram from TSP +#### Step 2: Fetch Cryptogram from Razorpay -Call Razorpay AI Commerce TSP 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. +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/ai-tpap/delegate/{delegate_id}/token_transactional_data +POST /v1/upi/delegate/{delegate_id}/cryptogram Authorization: Basic base64(key_id:key_secret) Content-Type: application/json @@ -429,6 +421,9 @@ Content-Type: application/json } ``` +> Exact API path subject to change. Refer to Razorpay developer documentation +> for the current endpoint. + **Response:** ```json @@ -554,14 +549,15 @@ When the business submits a `upi_circle` instrument with a 4. **Receives NPCI response** — SUCCESS, FAILURE, or PENDING. 5. **Sends webhook to business** — `payment.authorized` or `payment.failed`. -### Razorpay AI Commerce TSP: Cryptogram Issuance +### Razorpay as Credential Provider: Cryptogram Issuance -The TSP acts as the credential provider — analogous to Google Pay's token API -or Stripe's Shared Payment Token (SPT) API: +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: -| API | Method | Purpose | -| :-------------------------------------------------------- | :----- | :----------------------------------- | -| `POST /delegate/{delegate_id}/token_transactional_data` | POST | Issue a one-time cryptogram | +| Operation | Method | Purpose | +| :--------------------------- | :----- | :--------------------------- | +| Cryptogram issuance | POST | Issue a one-time cryptogram | **Response:** @@ -648,7 +644,7 @@ The cryptogram is: - [ ] `delegate_id` required on instrument; `cryptogram` required on credential. - [ ] Business profile at `/.well-known/ucp` declares `com.razorpay.upi.circle` with correct `business_id` and `environment`. - [ ] Platform completes delegation setup and holds a `delegate_id` with `status: linked`. -- [ ] Platform calls TSP to fetch cryptogram immediately before Complete Checkout. +- [ ] 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. @@ -665,11 +661,7 @@ The cryptogram is: | Resource | URL | | :------------------------------- | :------------------------------------------------------------------------------------- | -| Handler Spec | `https://razorpay.com/ucp/upi_circle/2026-03-26/` | -| Config Schema | `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/config.json` | -| Instrument Schema | `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/upi_circle_instrument.json` | -| Credential Schema | `https://razorpay.com/ucp/upi_circle/2026-03-26/schemas/upi_circle_credential.json` | -| Razorpay AI Commerce TSP API | `https://razorpay.com/docs/ucp/ai-tpap/` | +| 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) | diff --git a/source/handlers/razorpay-upi-circle/types/platform_config.json b/source/handlers/razorpay-upi-circle/types/platform_config.json index 296738371..74f89e553 100644 --- a/source/handlers/razorpay-upi-circle/types/platform_config.json +++ b/source/handlers/razorpay-upi-circle/types/platform_config.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/platform_config.json", "title": "Razorpay UPI Circle Platform Config", - "description": "Platform-level configuration for the com.razorpay.upi.circle payment handler. Unlike UPI Intent (which requires deep-link capability), UPI Circle requires the platform to hold a valid delegate_id and integrate with Razorpay AI Commerce TSP to fetch one-time cryptograms before each Complete Checkout.", + "description": "Platform-level configuration for the com.razorpay.upi.circle payment handler. Unlike UPI Intent (which requires deep-link capability), UPI Circle requires the platform to hold a valid delegate_id and integrate with Razorpay to fetch one-time cryptograms before each Complete Checkout.", "type": "object", "required": ["environment"], "properties": { diff --git a/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json index 9b34714a9..28eff1d7c 100644 --- a/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json +++ b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json @@ -7,7 +7,7 @@ "$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 AI Commerce TSP delegation setup.", + "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" } ], diff --git a/source/schemas/shopping/types/upi_circle_credential.json b/source/schemas/shopping/types/upi_circle_credential.json index 61a2a166c..6f3e51a2c 100644 --- a/source/schemas/shopping/types/upi_circle_credential.json +++ b/source/schemas/shopping/types/upi_circle_credential.json @@ -2,7 +2,7 @@ "$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 the PSP's AI Commerce TSP 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.", + "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" @@ -18,7 +18,7 @@ "cryptogram": { "type": "string", "minLength": 1, - "description": "One-time cryptogram value issued by the PSP's AI Commerce TSP. Single-use and time-limited — authorizes exactly one delegated debit. NPCI rejects reuse. The platform MUST fetch a fresh cryptogram for each transaction." + "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", diff --git a/source/schemas/shopping/types/upi_circle_instrument.json b/source/schemas/shopping/types/upi_circle_instrument.json index 201e48a9a..e9da51a6f 100644 --- a/source/schemas/shopping/types/upi_circle_instrument.json +++ b/source/schemas/shopping/types/upi_circle_instrument.json @@ -2,7 +2,7 @@ "$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 the PSP's AI Commerce TSP 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).", + "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" @@ -18,7 +18,7 @@ "delegate_id": { "type": "string", "minLength": 1, - "description": "Persistent identifier for the buyer's delegation mandate, obtained during one-time delegation setup with the PSP's AI Commerce TSP. Analogous to a card vault token — it references saved delegation credentials. Used by the platform to fetch a one-time cryptogram before each transaction." + "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", From bc5f6393ef9e1b6d50591fbeed3f62b13f5816ea Mon Sep 17 00:00:00 2001 From: Paras Kathuria Date: Tue, 31 Mar 2026 12:43:20 +0530 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20remove=20UPI=20Intent=20comparison?= =?UTF-8?q?=20=E2=80=94=20keep=20UPI=20Circle=20spec=20isolated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../razorpay-upi-circle-payment-handler.md | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/docs/specification/razorpay-upi-circle-payment-handler.md b/docs/specification/razorpay-upi-circle-payment-handler.md index d7284b3d2..1134881ca 100644 --- a/docs/specification/razorpay-upi-circle-payment-handler.md +++ b/docs/specification/razorpay-upi-circle-payment-handler.md @@ -38,17 +38,6 @@ at payment time. > bilateral integration — analogous to a buyer saving a card on ChatGPT. > See [Delegation Setup](#delegation-setup-outside-ucp). -### UPI Circle vs UPI Intent — Architecture Comparison - -| Property | UPI Intent (`com.razorpay.upi`) | UPI Circle (`com.razorpay.upi.circle`) | -| :------------------------------ | :-------------------------------------- | :----------------------------------------------- | -| **Per-transaction auth** | Yes — buyer enters MPIN every time | No — mandate pre-authorizes within limits | -| **Credential acquisition** | PSP-side (after Complete Checkout) | Platform-side (before Complete Checkout) | -| **UCP architecture fit** | Requires escalation / protocol amendment| Fits existing architecture — no changes needed | -| **Truly agentic** | No — buyer must interact per payment | Yes — agent pays autonomously | -| **Buyer experience** | App switch / QR scan per transaction | Zero-friction — like card-on-file | -| **UCP pattern analog** | Custom escalation flow | Same as `com.google.pay`, `dev.shopify.shop_pay` | - ### Key Benefits * **Truly agentic** — No per-transaction authentication. Agent pays autonomously within mandate limits. @@ -251,9 +240,8 @@ Before advertising this handler, businesses **MUST** complete: * `key_id` / `key_secret` — for webhook signature verification and PSP API calls. * `business_id` (`acc_*`) — the Razorpay account MID for handler config. -> **Note:** Unlike UPI Intent, no merchant VPA is needed in the handler config. -> The payee VPA is resolved by Razorpay PSP at payment time from the -> `delegate_id` and cryptogram. +> **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 @@ -582,8 +570,8 @@ The cryptogram is: | NPCI / Mandate Error | UCP `code` | `severity` | Platform Action | | :------------------------------------ | :----------------- | :-------------------- | :---------------------------------------------------- | -| Mandate limit exceeded (per-txn) | `payment_declined` | `requires_buyer_input`| Lower amount or suggest UPI Intent | -| Mandate limit exceeded (monthly) | `payment_declined` | `requires_buyer_input`| Wait for limit reset or suggest UPI Intent | +| 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 | @@ -665,7 +653,6 @@ The cryptogram is: | 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) | -| UPI Intent Handler (comparison) | [razorpay-upi-payment-handler.md](razorpay-upi-payment-handler.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` | From 95f58d52f9ccf4835d771061fca28361763cc30a Mon Sep 17 00:00:00 2001 From: Himanshu Shekhar Date: Tue, 31 Mar 2026 19:43:54 +0530 Subject: [PATCH 5/6] docs: enhance UPI Circle handler spec with platform integration and polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add platform handler integration JSON sample matching UCP profile format - Expand Step 5 response handling to cover all four shapes: completed, complete_in_progress, canceled, and requires_escalation - Replace naive polling loop with exponential backoff (2s→10s, ×1.5, 2min cap) including network error resilience and terminal status switch - Add error handling table mapping message types to platform actions - Fix AI Commerce TSP anchor link in integration guide table (line 52) --- .../examples/encrypted-credential-handler.md | 10 +- .../platform-tokenizer-payment-handler.md | 8 +- .../processor-tokenizer-payment-handler.md | 10 +- docs/specification/payment-handler-guide.md | 10 +- .../specification/payment-handler-template.md | 4 +- .../razorpay-upi-circle-payment-handler.md | 404 ++++++++++++------ .../handlers/razorpay-upi-circle/schema.json | 12 +- .../types/business_config.json | 35 +- .../types/platform_config.json | 23 +- .../types/response_config.json | 2 +- .../types/upi_circle_instrument.json | 2 +- 11 files changed, 335 insertions(+), 185 deletions(-) diff --git a/docs/specification/examples/encrypted-credential-handler.md b/docs/specification/examples/encrypted-credential-handler.md index 97b175fb5..1c3db1a37 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 `merchant_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 | +| `merchant_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", + "merchant_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 | +| `merchant_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", + "merchant_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..4b56e0fdc 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 | +| `merchant_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" + "merchant_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 | +| `merchant_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", + "merchant_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..8f181c45f 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 | +| `merchant_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" + "merchant_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 | +| `merchant_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" + "merchant_id": "merchant_xyz789" } } ``` @@ -206,7 +206,7 @@ business's configuration. ], "config": { "environment": "production", - "business_id": "merchant_xyz789" + "merchant_id": "merchant_xyz789" } } ] diff --git a/docs/specification/payment-handler-guide.md b/docs/specification/payment-handler-guide.md index 46cafc7df..da5b3cdf8 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., `merchant_id`, `merchant_name`). Specifications **MUST** explicitly document these field mappings. **Standard Participants:** @@ -229,7 +229,7 @@ and typically includes different configuration: ], "config": { "environment": "production", - "business_id": "business_xyz_789" + "merchant_id": "business_xyz_789" } } ``` @@ -274,7 +274,7 @@ and typically includes different configuration: "config": { "api_version": 2, "environment": "production", - "business_id": "business_xyz_789" + "merchant_id": "business_xyz_789" } } ``` @@ -428,7 +428,7 @@ Each variant has its own config schema tailored to its context: "enum": ["sandbox", "production"], "default": "production" }, - "business_id": { + "merchant_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": { + "merchant_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..56753c064 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., `merchant_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., merchant_id} | | {additional config} | {any additional configuration from onboarding} | ### Handler Configuration diff --git a/docs/specification/razorpay-upi-circle-payment-handler.md b/docs/specification/razorpay-upi-circle-payment-handler.md index 1134881ca..b16220c2a 100644 --- a/docs/specification/razorpay-upi-circle-payment-handler.md +++ b/docs/specification/razorpay-upi-circle-payment-handler.md @@ -16,26 +16,22 @@ # Razorpay UPI Circle Payment Handler -* **Handler Name:** `com.razorpay.upi.circle` +* **Handler Name:** `com.razorpay.upi_circle` * **Version:** `{{ ucp_version }}` ## Introduction -The `com.razorpay.upi.circle` handler enables businesses to accept **delegated +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, sends it in Complete Checkout, and the business processes it -straight to `completed`. No escalation, no app switch, no buyer interaction -at payment time. +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 — analogous to a buyer saving a card on ChatGPT. +> 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 @@ -48,11 +44,12 @@ at payment time. ### Integration Guide -| Participant | Section | -| :------------------- | :-------------------------------------------------------- | -| **Business** | [Business Integration](#business-integration) | -| **Platform** | [Platform Integration](#platform-integration) | -| **PSP (Razorpay)** | [PSP Integration](#psp-integration-razorpay) | +| 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) | --- @@ -60,14 +57,14 @@ at payment time. > **Note on Terminology:** This specification refers to the Razorpay merchant > account holder as the **"business."** Technical schema fields retain standard -> industry nomenclature `merchant_*` where applicable. +> 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; fetches cryptogram from Razorpay; submits in Complete Checkout | Yes — Razorpay API integration, active `delegate_id` | +| **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 (Credential Provider)** | Issues one-time cryptograms for established delegation mandates | N/A — credential infrastructure (analogous to Google Pay token API) | +| **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) | --- @@ -75,53 +72,56 @@ at payment time. 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 ChatGPT is not part of +— it is **not part of UCP**, just as saving a card on Gemini is not part of the checkout protocol. ### Setup Flow ``` -+------------+ +---------------------------+ +-----------+ -| Platform | | Razorpay | | Buyer UPI | -+-----+------+ +-------------+-------------+ | App | - | | +-----+-----+ - | | | - [Buyer initiates delegation setup on Platform] | - | | | - | 1. POST /customers/generate_otp | | - | { mobile: "+91XXXXXXXXXX" } | | - |----------------------------------->| | - | |-- OTP SMS to buyer's mobile ->| - | 2. { customer_id } | | - |<-----------------------------------| | - | | | - [Buyer enters OTP on Platform] | - | | | - | 3. POST /customers/{id}/otp_submit | | - | { otp: "XXXXXX" } | | - |----------------------------------->| | - | 4. { intent_link, qr_code } | | - |<-----------------------------------| | - | | | - [Platform opens intent link or shows QR — buyer opens in UPI app] | - | | | - | | 5. Buyer configures | - | | delegation limits, | - | | enters MPIN | - | |<------------------------------| - | | | - | | 6. Bank + NPCI confirm | - | | mandate created (UMN) | - | |<- - - - - - - - - - - - - - - | - | | | - | 7. GET /delegate/{delegate_id} | | - | (poll until status: linked) | | - |----------------------------------->| | - | 8. { status: "linked", | | - | delegate_id: "c894aa29..." } | | - |<-----------------------------------| | - | | | - [Platform stores delegate_id — used for all future payments] ++----------+ +---------------------+ +---------------------+ +---------+ +| 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 @@ -135,8 +135,8 @@ the checkout protocol. **Output:** `delegate_id` — persistent mandate reference. All subsequent payments use this ID to fetch cryptograms. -> Exact API paths are published in Razorpay's documentation. Refer to the -> Razorpay developer docs for the current endpoint reference. +> Get in touch with Razorpay for API Docs + --- @@ -149,8 +149,8 @@ payments use this ID to fetch cryptograms. | Platform | | Razorpay (Cred. Provider)| | Business | | Razorpay (PSP) | +-----+------+ +-------------+-------------+ +--------+---------+ +--------+---------+ | | | | - ════╪════════════════════════════╪════════════════════════════╪══════════════════════╪════ - PRE-CONDITION: Delegation setup completed out-of-band. + ═════════════════════════════════╪════════════════════════════╪══════════════════════╪════ + PRE-CONDITION: Delegation setup completed . Platform holds a valid delegate_id (status: linked). ════╪════════════════════════════╪════════════════════════════╪══════════════════════╪════ | | | | @@ -161,23 +161,23 @@ payments use this ID to fetch cryptograms. | | | | | 3. POST /checkout | | | | (create) | | | - |-------------------------------------------------------------->| | + |-------------------------------------------------------->| | | 4. Checkout response | | | | (upi_circle available) | | | - |<--------------------------------------------------------------| | + |<--------------------------------------------------------| | + | | | | + [Platform selects upi_circle for buyer with active delegate_id] | | | | | - [Platform selects upi_circle for buyer with active delegate_id] + | 5. GET payment cryptogram | | | + | for the saved | | | + | delegation id. | | | | | | | - | 5. POST /delegate/ | | | - | {delegate_id}/ | | | - | token_transactional_ | | | - | data | | | |--------------------------->| | | | 6. { cryptogram, | | | | expires_at } | | | |<---------------------------| | | | | | | - [Platform builds instrument + credential, submits Complete Checkout] + [Platform builds instrument + credential, submits Complete Checkout] | | | | | | 7. POST checkout/complete | | | | instrument: | | | @@ -188,28 +188,17 @@ payments use this ID to fetch cryptograms. | cryptogram | | | | cryptogram: a345345... | | | | expires_at: ... | | | - |-------------------------------------------------------------->| | + |-------------------------------------------------------->| | | | | | - | | | 8. Validate handler | - | | | Validate idempot.| - | | | Extract cryptogram| + | | | 8.Validate handler | + | | | Validate idempot. | + | | | Extract cryptogram| | | | | - | | | 9. Process delegated| - | | | debit (delegate_ | - | | | id + cryptogram) | + | | | 9. Process payments | + | | | through PSP | + | | (delegate_id + cryptogram) | | |--------------------->| | | | | - | | | [Razorpay resolves | - | | | UMN from | - | | | delegate_id] | - | | | | - | | ══════════╪══════════════════════╪═══ - | | NPCI routes delegated debit: - | | Razorpay → NPCI → Remitter Bank - | | (Primary account holder's bank) - | | No MPIN required. - | | ══════════╪══════════════════════╪═══ - | | | | | | | 10. Webhook: | | | | payment.authorized | | | |<---------------------| @@ -219,7 +208,7 @@ payments use this ID to fetch cryptograms. | | | completed | | | | | | 12. status: completed | | | - |<--------------------------------------------------------------| | + |<--------------------------------------------------------| | | (or complete_in_ | | | | progress → poll) | | | ``` @@ -238,7 +227,6 @@ Before advertising this handler, businesses **MUST** complete: `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. - * `business_id` (`acc_*`) — the Razorpay account MID for handler config. > **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. @@ -247,18 +235,27 @@ Before advertising this handler, businesses **MUST** complete: #### Handler Schema -| Config Variant | Context | Key Fields | -| :---------------- | :------------------- | :-------------------------- | -| `business_config` | Business discovery | `business_id`, `environment`| -| `platform_config` | Platform discovery | `environment` | -| `response_config` | Checkout responses | `environment` | +| 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 | -| :------------ | :----- | :------- | :------------------------------------------------------- | -| `business_id` | string | Yes | Razorpay account identifier (`acc_*`) | -| `environment` | string | Yes | `test` or `production` | +| 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 @@ -267,12 +264,12 @@ Before advertising this handler, businesses **MUST** complete: "ucp": { "version": "{{ ucp_version }}", "payment_handlers": { - "com.razorpay.upi.circle": [ + "com.razorpay.upi_circle": [ { - "id": "razorpay_upi_circle_primary", - "name": "com.razorpay.upi.circle", + "id": "razorpay_upi_circle", + "name": "com.razorpay.upi_circle", "config": { - "business_id": "acc_KN2V9oR1g9n0qH", + "key_id": "rzp_live_KN2V9UHAlkas", "environment": "production" } } @@ -288,7 +285,7 @@ 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. + `com.razorpay.upi_circle` handler. 2. **Ensure idempotency.** If this `checkout_id` has already been processed, return the previous result without re-processing. @@ -298,9 +295,10 @@ Upon receiving a Complete Checkout with a `upi_circle` instrument and 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`). + `delegate_id`, `cryptogram`). +5. **Receive Razorpay Webhook** On the configured URL -5. **Return result.** +6. **Return result.** **Success Response:** @@ -362,38 +360,85 @@ def verify_razorpay_webhook(body: bytes, signature: str, webhook_secret: str) -> ### Prerequisites -Before handling `com.razorpay.upi.circle` payments, platforms **MUST**: +Before handling `com.razorpay.upi_circle` payments, platforms **MUST**: -1. **Complete delegation setup** — Buyer must have an active delegation mandate - via Razorpay. Platform must hold a valid `delegate_id` with `status: linked`. -2. **Integrate with Razorpay** — Ability to call Razorpay's cryptogram issuance - API to fetch a one-time cryptogram per transaction. +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 when: +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 +Identify `com.razorpay.upi_circle` in the business's `payment.handlers` array from the Create Checkout response. ```json { - "id": "razorpay_upi_circle_primary", - "name": "com.razorpay.upi.circle", + "id": "razorpay_upi_circle", + "name": "com.razorpay.upi_circle", "config": { - "business_id": "acc_KN2V9oR1g9n0qH", + "key_id": "rzp_live_KN2V9UHAlkas", "environment": "production" } } ``` -#### Step 2: Fetch Cryptogram from Razorpay +#### 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 @@ -425,12 +470,12 @@ Content-Type: application/json > 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 +#### Step 3: Build Instrument + Credential (Minting Credential) ```json { "id": "pi_001", - "handler_id": "razorpay_upi_circle_primary", + "handler_id": "razorpay_upi_circle", "type": "upi_circle", "delegate_id": "c894aa29a7da69", "selected": true, @@ -459,7 +504,7 @@ UCP-Agent: profile="https://agent.example/profile" "instruments": [ { "id": "pi_001", - "handler_id": "razorpay_upi_circle_primary", + "handler_id": "razorpay_upi_circle", "type": "upi_circle", "delegate_id": "c894aa29a7da69", "selected": true, @@ -485,6 +530,8 @@ UCP-Agent: profile="https://agent.example/profile" #### Step 5: Handle Response +The Complete Checkout response falls into one of four shapes: + **Synchronous success** — Business responds directly with `completed`: ```json @@ -498,28 +545,108 @@ UCP-Agent: profile="https://agent.example/profile" } ``` -**Async processing** — Business returns `complete_in_progress`. Platform polls: +**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 -async function pollCheckout(checkoutId) { - for (let i = 0; i < 60; i++) { - await sleep(2000); - const checkout = await getCheckout(checkoutId); +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 +}; - if (checkout.status === "completed") { - return { success: true, order: checkout.order }; +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; } - if (checkout.status === "canceled") { - return { success: false, messages: checkout.messages }; + + 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" }; + + return { success: false, reason: "timeout", checkoutId }; } ``` -**Failure** — Platform reads `messages`. For `recoverable` errors, the platform -MAY retry with a fresh cryptogram. For `requires_buyer_input` errors (limit -exceeded, mandate expired), surface a message to the buyer. +#### 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. --- @@ -537,7 +664,7 @@ When the business submits a `upi_circle` instrument with a 4. **Receives NPCI response** — SUCCESS, FAILURE, or PENDING. 5. **Sends webhook to business** — `payment.authorized` or `payment.failed`. -### Razorpay as Credential Provider: Cryptogram Issuance +### 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 @@ -545,8 +672,11 @@ 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 @@ -630,7 +760,7 @@ The cryptogram is: - [ ] 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 `business_id` and `environment`. +- [ ] 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. diff --git a/source/handlers/razorpay-upi-circle/schema.json b/source/handlers/razorpay-upi-circle/schema.json index 7f928c8bf..b6a09e714 100644 --- a/source/handlers/razorpay-upi-circle/schema.json +++ b/source/handlers/razorpay-upi-circle/schema.json @@ -2,8 +2,8 @@ "$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", + "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": { @@ -11,10 +11,10 @@ "$ref": "types/upi_circle_instrument.json" }, - "com.razorpay.upi.circle": { + "com.razorpay.upi_circle": { "payment_instrument": { "title": "Razorpay UPI Circle Payment Instrument", - "description": "UPI Circle instrument for com.razorpay.upi.circle.", + "description": "UPI Circle instrument for com.razorpay.upi_circle.", "$ref": "#/$defs/upi_circle_instrument" }, @@ -30,7 +30,7 @@ "properties": { "config": { "$ref": "types/platform_config.json", - "description": "Platform configuration for com.razorpay.upi.circle." + "description": "Platform configuration for com.razorpay.upi_circle." } } } @@ -49,7 +49,7 @@ "properties": { "config": { "$ref": "types/business_config.json", - "description": "Business configuration for com.razorpay.upi.circle." + "description": "Business configuration for com.razorpay.upi_circle." } } } diff --git a/source/handlers/razorpay-upi-circle/types/business_config.json b/source/handlers/razorpay-upi-circle/types/business_config.json index e46c8a012..6a2d2927a 100644 --- a/source/handlers/razorpay-upi-circle/types/business_config.json +++ b/source/handlers/razorpay-upi-circle/types/business_config.json @@ -1,19 +1,30 @@ -{ + "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/business_config.json", - "title": "Razorpay UPI Circle Business Config", - "description": "Business-level configuration for the com.razorpay.upi.circle payment handler. Declared in the business's UCP profile. Unlike UPI Intent, no merchant VPA is required here — payee VPA is resolved by Razorpay PSP at payment processing time using the delegate_id and cryptogram.", + "$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": ["business_id", "environment"], + "required": ["environment", "key_id"], "properties": { - "business_id": { + "environment": { "type": "string", - "description": "Razorpay account identifier (MID) for the business. Used by Razorpay PSP to route the delegated debit to the correct merchant." + "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_)." }, - "environment": { + "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", - "enum": ["test", "production"], - "description": "Razorpay API environment. Use 'test' for sandbox testing, 'production' for live payments." + "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 index 74f89e553..919349b52 100644 --- a/source/handlers/razorpay-upi-circle/types/platform_config.json +++ b/source/handlers/razorpay-upi-circle/types/platform_config.json @@ -1,15 +1,24 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://razorpay.com/ucp/handlers/upi-circle/types/platform_config.json", - "title": "Razorpay UPI Circle Platform Config", - "description": "Platform-level configuration for the com.razorpay.upi.circle payment handler. Unlike UPI Intent (which requires deep-link capability), UPI Circle requires the platform to hold a valid delegate_id and integrate with Razorpay to fetch one-time cryptograms before each Complete Checkout.", + "$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": ["test", "production"], - "description": "Razorpay API environment. Must match the business config environment." + "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 index c9345b952..3c3bd7bae 100644 --- a/source/handlers/razorpay-upi-circle/types/response_config.json +++ b/source/handlers/razorpay-upi-circle/types/response_config.json @@ -2,7 +2,7 @@ "$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.", + "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": { diff --git a/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json index 28eff1d7c..f8fe7a5ba 100644 --- a/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json +++ b/source/handlers/razorpay-upi-circle/types/upi_circle_instrument.json @@ -2,7 +2,7 @@ "$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.", + "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": { From 634469d26c082649354624561c3d4c6d067bb75d Mon Sep 17 00:00:00 2001 From: Himanshu Shekhar Date: Wed, 1 Apr 2026 11:31:47 +0530 Subject: [PATCH 6/6] chore: updated docs --- docs/specification/checkout-rest.md | 8 ++++---- .../examples/encrypted-credential-handler.md | 10 +++++----- .../examples/platform-tokenizer-payment-handler.md | 8 ++++---- .../examples/processor-tokenizer-payment-handler.md | 10 +++++----- docs/specification/overview.md | 2 +- docs/specification/payment-handler-guide.md | 12 ++++++------ docs/specification/payment-handler-template.md | 4 ++-- docs/specification/playground.md | 2 +- 8 files changed, 28 insertions(+), 28 deletions(-) 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 1c3db1a37..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 `merchant_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`) | -| `merchant_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", - "merchant_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 | -| `merchant_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", - "merchant_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 4b56e0fdc..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`) | -| `merchant_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", - "merchant_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 | -| `merchant_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", - "merchant_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 8f181c45f..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`) | -| `merchant_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", - "merchant_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 | -| `merchant_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", - "merchant_id": "merchant_xyz789" + "key_id": "merchant_xyz789" } } ``` @@ -206,7 +206,7 @@ business's configuration. ], "config": { "environment": "production", - "merchant_id": "merchant_xyz789" + "key_id": "merchant_xyz789" } } ] diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 14aea05c7..dff6c8519 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -1253,7 +1253,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 da5b3cdf8..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 **`key_*`** (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", - "merchant_id": "business_xyz_789" + "key_id": "rzp_live_ABCD" } } ``` @@ -274,7 +274,7 @@ and typically includes different configuration: "config": { "api_version": 2, "environment": "production", - "merchant_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" }, - "merchant_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"] }, - "merchant_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 56753c064..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 -> **`key_*`** (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., merchant_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...." },