Available in🇮🇳 India
Create QR payments, simulate failures and reconcile against the webhooks in test mode, then check the scan and approve journey on real devices before you go live.
This page covers one-time UPI QR payments on S2S: you take the Intent upi:// link and render it as a QR code, typically on desktop. The customer scans it with any UPI app. QR uses the same API calls and test triggers as Intent.
UPI flows: Intent · QR · Collect
Start with the overview for test keys and webhook setup.
1. Test Mode Coverage
How live mode and test mode differ
What you can test
Endpoint coverage in test mode
Webhooks: what fires, and what it lets you decide
Every webhook this flow fires in live mode is listed below, along with whether it also fires in test mode. Test mode webhooks have the same shape as live mode ones and are delivered to the URL set in your test mode Dashboard.
Which webhooks each scenario fires
Before you trust any of them
- Verify the signature on every event. Replay one event to your endpoint and send one with a changed body. The replay must be ignored and the tampered event rejected. An endpoint that skips this is an open write to your order state.
- Expect duplicates. The same event can arrive more than once. Key your handler on the event ID and make it idempotent.
- Order is not guaranteed across events. Do not assume
payment.capturedlands beforeorder.paid; handle whichever arrives first. - Webhook first, callback second. The browser callback can be lost, blocked or forged. Fulfil on the webhook.
2. Before you begin
- Generate your test mode API keys from the Dashboard. Keys that start with
rzp_test_send all requests to test mode. You do not need to enable any separate flag. - Use the same APIs and request formats you use in live mode. Only the API keys change.
- Pass every amount in paise, as you do in live mode. For example, send
201for ₹2.01 and100for ₹1. - Configure a webhook URL in your test mode Dashboard to receive test events. Test mode webhooks are separate from live mode ones and do not carry over.
100 (₹1) and the payment succeeds. Send one of the reserved amounts and the payment fails with the error mapped to that amount. For example, create the order for 201 (₹2.01) and the payment always fails with an insufficient balance error, so you can check that your code handles that case without waiting for a real account to run out of money.
- Send
100(₹1) on both the order and the payment to succeed. - Send a reserved amount from 3.3 UPI error scenarios on both calls to fail with the error mapped to that amount.
- You do not need to scan the QR you render.
- Any amount that is not in the reserved list succeeds.
3. Scenarios to Test Your Integration
This section lists every scenario we think is worth running against your UPI QR integration before you go live. They are grouped by what each one proves, rather than by how urgent they are, so that you can work down the page in order and know that you have covered the integration end to end. Must-have. We highly recommend running these scenarios first. They cover the successful path and the handful of outcomes you are almost certain to meet once you are live, so getting through them is the shortest route to an integration you can trust. Other common scenarios. These come up in normal day-to-day running and are shared across one-time UPI and UPI Autopay, so the same handling has to work on both. They are not errors as such, but your integration still needs to recognise them and respond correctly, so we recommend covering them before launch. Error scenarios. These are failures you can bring on deliberately, using a reserved trigger, so that you can check your error handling without waiting for a real failure to happen. Most rows use a single trigger; where a row lists two, each one is shown separately in the error object column. Not available in test mode yet. These scenarios are real and your integration still has to handle them, but they cannot be reproduced in test mode today. We recommend reading through them and checking your handling on live traffic or in a controlled live test before you ship.3.1 Must-build UPI scenarios
| # | Scenario | Trigger creds | Steps to follow |
|---|---|---|---|
| 1 | Successful payment | amount: 100 | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Verify the signature on your server. Confirm payment.captured. Webhooks received: payment.authorized, payment.captured, order.paid. Status changes: the payment moves to captured and the order to paid. |
| 2 | Customer cancels or declines in the app | 302, 304 | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm payment.failed with payment_cancelled / payment_declined. The order stays open. |
| 3 | Request times out | 105 | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm payment_timed_out. Stop polling and let the customer retry. |
| 4 | Fetch payment and reconcile (all methods) | A payment ID from 1 (success) and one from any row in 3.3 UPI error scenarios | Poll GET /v1/payments/:id while the customer is in the UPI app. Confirm the status matches the webhook. Repeat scenario 1 with each reserved amount to cover the error scenarios in 3.3, and replace the QR with the result once the payment is final (scenario 10). Scanning the QR is client-side, so check that on real devices. |
3.2 Common scenarios in UPI
| # | Scenario | Trigger creds | Steps to follow |
|---|---|---|---|
| 5 | Full and partial refund (all methods) | A captured payment from 1 | Refund in full and in part. Confirm refund.processed. |
3.3 UPI error scenarios
Bank and network (1xx) The payment reached the rails and something outside the customer’s control went wrong. These are temporary, so offer the customer the same method again.| # | Scenario | Trigger creds | Error object | Steps to follow |
|---|---|---|---|---|
| 6 | Bank technical error | 102, 104 | on 102code GATEWAY_ERRORsource issuer_bankstep payment_debit_requestreason bank_technical_erroron 104 code GATEWAY_ERRORsource issuer_bankstep payment_debit_responsereason bank_technical_error | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm bank_technical_error and a retry option. |
| 7 | Gateway technical error | 101, 103 | on 101code GATEWAY_ERRORsource gatewaystep payment_responsereason gateway_technical_erroron 103 code GATEWAY_ERRORsource gatewaystep payment_responsereason gateway_technical_error | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm gateway_technical_error. |
| 8 | UPI app / PSP not available | 107 | code GATEWAY_ERRORsource customer_pspstep payment_requestreason upi_app_not_available | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm upi_app_not_available and a retry option. |
| 9 | Stop showing the QR at a final state | amount: 100, then 302 | on 302code BAD_REQUEST_ERRORsource customerstep payment_authenticationreason payment_cancelled | Create an order with the trigger amount. Render the QR. Confirm the QR is replaced by the result on payment.captured / payment.failed. |
| # | Scenario | Trigger creds | Error object | Steps to follow |
|---|---|---|---|---|
| 10 | Insufficient funds | 201 | code GATEWAY_ERRORsource customerstep payment_debit_requestreason insufficient_funds | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm insufficient_funds and a “use another account” message. |
| 11 | Wrong UPI PIN | 204 | code BAD_REQUEST_ERRORsource customerstep payment_authenticationreason incorrect_pin | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm incorrect_pin and a retry option. |
| 12 | Per-transaction limit exceeded | 208 | code BAD_REQUEST_ERRORsource customerstep payment_debit_responsereason transaction_limit_exceeded | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm transaction_limit_exceeded and an “amount too high” message. |
| 13 | Daily / net debit limit exceeded | 209 | code BAD_REQUEST_ERRORsource customerstep payment_debit_responsereason transaction_limit_exceeded | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm transaction_limit_exceeded and a “daily limit reached, try again tomorrow” message. |
| 14 | UPI PIN not set | 205 | code BAD_REQUEST_ERRORsource customerstep payment_authenticationreason pin_not_set | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm pin_not_set and a “set your UPI PIN” message, not a retry-PIN prompt. |
| 15 | UPI PIN attempts exceeded (locked) | 206 | code BAD_REQUEST_ERRORsource customerstep payment_authenticationreason pin_attempts_exceeded | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm pin_attempts_exceeded and a “try later or use another method” message, not a retry-PIN prompt. |
| 16 | Remitter account blocked | 212 | code BAD_REQUEST_ERRORsource issuer_bankstep payment_debit_requestreason debit_instrument_blocked | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm debit_instrument_blocked and an “account blocked, use another” message. |
| 17 | Beneficiary account does not exist | 213 | code GATEWAY_ERRORsource gatewaystep payment_credit_requestreason beneficiary_account_does_not_exist | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm beneficiary_account_does_not_exist (receiver account inactive). |
| 18 | Transaction frequency limit exceeded | 210 | code BAD_REQUEST_ERRORsource businessstep payment_initiationreason transaction_frequency_limit_exceeded | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm transaction_frequency_limit_exceeded and a “try again later” message. |
| 19 | Registered mobile number not found | 202 | code BAD_REQUEST_ERRORsource customerstep payment_requestreason mobile_number_invalid | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm mobile_number_invalid and a “use another method” message. |
| 20 | Invalid bank account | 203 | code BAD_REQUEST_ERRORsource customerstep payment_debit_requestreason bank_account_invalid | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm bank_account_invalid and a “use another account” message. |
| # | Scenario | Trigger creds | Error object | Steps to follow |
|---|---|---|---|---|
| 21 | Declined by bank or risk check | 402, 404 | on 402code GATEWAY_ERRORsource issuer_bankstep payment_debit_requestreason payment_declinedon 404 code GATEWAY_ERRORsource issuer_bankstep payment_debit_responsereason payment_risk_check_failed | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm payment_risk_check_failed / payment_declined. |
| 22 | API validation errors (all methods) | Missing amount, amount: 0, wrong key secret | — | Send each bad request. Confirm HTTP 4xx, no payment created, no webhook. |
| 23 | Invalid UPI ID (VPA resolution failed) | 401 | code GATEWAY_ERRORsource networkstep payment_requestreason vpa_resolution_failed | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm vpa_resolution_failed and an “incorrect UPI ID” message. |
| 24 | Invalid device | 407 | code BAD_REQUEST_ERRORsource customerstep payment_authenticationreason invalid_device | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm invalid_device and a “use another method” message. |
| 25 | Duplicate request | 406 | code BAD_REQUEST_ERRORsource gatewaystep payment_requestreason duplicate_request | Create an order with the trigger amount. Create the payment: POST /v1/payments/create/upi with upi.flow: "intent". Render the upi:// link as a QR code. Confirm duplicate_request (temporary) and allow retry. |
3.4 Not available in test mode yet
4. Common Integration Gaps to Avoid
These are the mistakes that most often cause failed payments, missed debits or double charges after go-live. “Test with” points to the scenario that proves you’ve handled it. Webhook and token updates
Error handling and messaging to the payer
Fetch API when webhooks are late
Capture & refunds
5. Best Integration Practices
These apply to every Razorpay integration. “On this method” says what each one means here; “Test it” links to where it is tested once for every method.
All best practices: Overview · Webhooks and Fetch APIs · Payment states and late authorisation · Error handling · Downtime handling · Security