> ## Documentation Index
> Fetch the complete documentation index at: https://razorpay.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# UPI QR: How to Test Your Integration

> Test one-time UPI QR payments on your S2S integration in Test Mode, including success and failure scenarios.

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](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/upi-intent.md) · QR · [Collect](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/upi-collect.md)

Start with the [overview](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s.md) for test keys and webhook setup.

## 1. Test Mode Coverage

### How live mode and test mode differ

| Behaviour | Live mode | Test mode |
| - | - | - |
| Customer approval | The customer scans the QR with any UPI app on any device and approves the payment there with their UPI PIN. | The simulator approves or declines on its own. Nothing is scanned, and the outcome is decided by the amount you send rather than by anything the customer does. |
| QR | You render the `upi://` link returned on the payment as a QR image and show it at checkout. | The link is returned with the same shape, so render it exactly as you would in live mode and check that the image your code produces is scannable. Nothing is scanned here, so the outcome still comes from the amount. |
| Scanning | A real UPI app on the customer's phone reads the QR off your checkout screen. | Client-side. Not part of test mode. Whether your QR actually scans, at the size and contrast you render it, has to be checked on real devices. |
| Fund source (`payer_account_type`) | The customer pays from whatever their UPI app is set to use, so `payer_account_type` comes back as `bank_account`, `credit_card`, `credit_line` or `ppiwallet`. | Always `bank_account`. Nothing you send changes it, so you cannot exercise the branches of your code that key off a credit card, a credit line or a wallet. Read the field, but verify the branching against live traffic. |
| Late authorisation | A payment with no bank response is marked `failed`, but Razorpay keeps polling the bank for up to 3 days. If the bank confirms late, the payment becomes `authorized` and `payment.authorized` arrives long after you showed the customer a failure. | Not simulated. There is no way to make a payment authorise late in test mode, so the handler that has to cope with a success arriving after a failure is one you have to verify in a controlled live test. |
| Webhook delivery | Razorpay sends the webhook once the gateway has authorised the payment. That round trip is real, so delivery can take up to 12 minutes and your reconciler has to allow for it. | The gateway is simulated, so the webhook is sent as soon as the create-payment request succeeds, almost instantly. Do not use test mode to size your reconciliation windows or to judge how long you should wait before polling. |

### What you can test

| Area | What you can test |
| - | - |
| Payments | Successful payments on the QR flow, and failure scenarios, using the calls in [Scenarios to test your integration](#3-scenarios-to-test-your-integration) and the reserved amounts in [3.3 UPI error scenarios](#3-3-upi-error-scenarios). |
| Reconciliation | Fetching a payment by ID and fetching the payments on an order, so you can prove your reconciler agrees with the webhook. |
| Capture and refunds | Capturing an authorised payment and refunding a captured one, in full and in part. |
| Webhooks | Payment, order and refund webhooks for each of the above, delivered to the URL configured in your test mode Dashboard. See [Webhooks: what fires, and what it lets you decide](#webhooks-what-fires-and-what-it-lets-you-decide) for which events fire today. |
| Rendering and scanning the QR | Rendering is yours to check here, since the link comes back with the same shape. Whether the image actually scans has to be checked on real devices. |

### Endpoint coverage in test mode

| API | Endpoint | Available on test mode |
| - | - | - |
| Create an order | `POST /v1/orders` | Available |
| Create a payment | `POST /v1/payments/create/upi` | Available |
| Fetch a payment | `GET /v1/payments/:payment_id` | Available |
| Fetch payments on an order | `GET /v1/orders/:order_id/payments` | Available |
| Capture a payment | `POST /v1/payments/:payment_id/capture` | Available |
| Refund a payment | `POST /v1/payments/:payment_id/refund` | Available |
| Fetch downtimes | `GET /v1/payments/downtimes` | Coming soon |

### 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.

| Webhook | Fires when | In test mode |
| - | - | - |
| `payment.authorized` | The payment is authorised. The money is held, not captured. | Available |
| `payment.captured` | The payment is captured, when the order was created with `payment_capture` set to true. | Available |
| `payment.failed` | The payment attempt fails. | Available |
| `order.paid` | The order is fully paid. | Available |
| `refund.processed` | A refund has left Razorpay. | Available |
| `refund.failed` | A refund could not be processed. | Coming soon |
| `payment.downtime.started, updated, resolved` | A method, bank or UPI app goes down and recovers. | Coming soon |

**Which webhooks each scenario fires**

| Scenario | Webhooks | In test mode |
| - | - | - |
| 1 Successful payment | `payment.authorized`, `payment.captured`, `order.paid` | Available |
| 2 Customer cancels or declines in the app | `payment.failed` | Available |
| 5 Full and partial refund | `refund.processed` | Available |
| 10 Stop showing the QR at a final state | `payment.captured`, `payment.failed` | Available |
| 26 Late authorisation | `payment.authorized` | Coming soon |

**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.captured` lands before `order.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 `201` for ₹2.01 and `100` for ₹1.
* Configure a [webhook](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/webhooks-and-fetch-apis.md) URL in your test mode Dashboard to receive test events. Test mode webhooks are separate from live mode ones and do not carry over.

In test mode the amount you send decides the outcome, because there is no real bank on the other end. Send `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](#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.

Amounts are in paise and follow one pattern: 1xx bank and network, 2xx account and limits, 3xx customer action, 4xx validation and risk.

## 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

<table>
  <colgroup>
    <col width="6%" />

    <col width="18%" />

    <col width="14%" />

    <col width="62%" />
  </colgroup>

  <thead><tr><th>#</th><th>Scenario</th><th>Trigger creds</th><th>Steps to follow</th></tr></thead>

  <tbody>
    <tr><td>1</td><td>Successful payment</td><td>`amount: 100`</td><td>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`.</td></tr>
    <tr><td>2</td><td>Customer cancels or declines in the app</td><td>`302`, `304`</td><td>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.</td></tr>
    <tr><td>3</td><td>Request times out</td><td>`105`</td><td>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.</td></tr>
    <tr><td>4</td><td>Fetch payment and reconcile (all methods)</td><td>A payment ID from 1 (success) and one from any row in 3.3 UPI error scenarios</td><td>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.</td></tr>
  </tbody>
</table>

### 3.2 Common scenarios in UPI

<table>
  <colgroup>
    <col width="6%" />

    <col width="18%" />

    <col width="14%" />

    <col width="62%" />
  </colgroup>

  <thead><tr><th>#</th><th>Scenario</th><th>Trigger creds</th><th>Steps to follow</th></tr></thead>

  <tbody>
    <tr><td>5</td><td>Full and partial refund (all methods)</td><td>A captured payment from 1</td><td>Refund in full and in part. Confirm `refund.processed`.</td></tr>
  </tbody>
</table>

### 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.

<table>
  <colgroup>
    <col width="5%" />

    <col width="18%" />

    <col width="10%" />

    <col width="22%" />

    <col width="45%" />
  </colgroup>

  <thead><tr><th>#</th><th>Scenario</th><th>Trigger creds</th><th>Error object</th><th>Steps to follow</th></tr></thead>

  <tbody>
    <tr><td>6</td><td>Bank technical error</td><td>`102`, `104`</td><td>on 102\
    `code` GATEWAY\_ERROR\
    `source` issuer\_bank\
    `step` payment\_debit\_request\
    `reason` bank\_technical\_error\
    \
    on 104\
    `code` GATEWAY\_ERROR\
    `source` issuer\_bank\
    `step` payment\_debit\_response\
    `reason` bank\_technical\_error</td><td>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.</td></tr>
    <tr><td>7</td><td>Gateway technical error</td><td>`101`, `103`</td><td>on 101\
    `code` GATEWAY\_ERROR\
    `source` gateway\
    `step` payment\_response\
    `reason` gateway\_technical\_error\
    \
    on 103\
    `code` GATEWAY\_ERROR\
    `source` gateway\
    `step` payment\_response\
    `reason` gateway\_technical\_error</td><td>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`.</td></tr>
    <tr><td>8</td><td>UPI app / PSP not available</td><td>`107`</td><td>`code` GATEWAY\_ERROR\
    `source` customer\_psp\
    `step` payment\_request\
    `reason` upi\_app\_not\_available</td><td>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.</td></tr>
    <tr><td>9</td><td>Stop showing the QR at a final state</td><td>`amount: 100`, then `302`</td><td>on 302\
    `code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_authentication\
    `reason` payment\_cancelled</td><td>Create an order with the trigger amount. Render the QR. Confirm the QR is replaced by the result on `payment.captured` / `payment.failed`.</td></tr>
  </tbody>
</table>

**Account and limits (2xx)**

The customer's account, their PIN or their bank's limits stopped the debit. A few are recoverable on the same account, a wrong PIN above all; most are not. Read the reason before you offer a retry.

<table>
  <colgroup>
    <col width="5%" />

    <col width="18%" />

    <col width="10%" />

    <col width="22%" />

    <col width="45%" />
  </colgroup>

  <thead><tr><th>#</th><th>Scenario</th><th>Trigger creds</th><th>Error object</th><th>Steps to follow</th></tr></thead>

  <tbody>
    <tr><td>10</td><td>Insufficient funds</td><td>`201`</td><td>`code` GATEWAY\_ERROR\
    `source` customer\
    `step` payment\_debit\_request\
    `reason` insufficient\_funds</td><td>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.</td></tr>
    <tr><td>11</td><td>Wrong UPI PIN</td><td>`204`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_authentication\
    `reason` incorrect\_pin</td><td>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.</td></tr>
    <tr><td>12</td><td>Per-transaction limit exceeded</td><td>`208`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_debit\_response\
    `reason` transaction\_limit\_exceeded</td><td>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.</td></tr>
    <tr><td>13</td><td>Daily / net debit limit exceeded</td><td>`209`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_debit\_response\
    `reason` transaction\_limit\_exceeded</td><td>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.</td></tr>
    <tr><td>14</td><td>UPI PIN not set</td><td>`205`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_authentication\
    `reason` pin\_not\_set</td><td>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.</td></tr>
    <tr><td>15</td><td>UPI PIN attempts exceeded (locked)</td><td>`206`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_authentication\
    `reason` pin\_attempts\_exceeded</td><td>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.</td></tr>
    <tr><td>16</td><td>Remitter account blocked</td><td>`212`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` issuer\_bank\
    `step` payment\_debit\_request\
    `reason` debit\_instrument\_blocked</td><td>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.</td></tr>
    <tr><td>17</td><td>Beneficiary account does not exist</td><td>`213`</td><td>`code` GATEWAY\_ERROR\
    `source` gateway\
    `step` payment\_credit\_request\
    `reason` beneficiary\_account\_does\_not\_exist</td><td>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).</td></tr>
    <tr><td>18</td><td>Transaction frequency limit exceeded</td><td>`210`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` business\
    `step` payment\_initiation\
    `reason` transaction\_frequency\_limit\_exceeded</td><td>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.</td></tr>
    <tr><td>19</td><td>Registered mobile number not found</td><td>`202`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_request\
    `reason` mobile\_number\_invalid</td><td>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.</td></tr>
    <tr><td>20</td><td>Invalid bank account</td><td>`203`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_debit\_request\
    `reason` bank\_account\_invalid</td><td>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.</td></tr>
  </tbody>
</table>

**Validation and risk (4xx)**

The request or the customer failed a check before or during the debit. Read the reason before you decide whether a retry is worth anything.

<table>
  <colgroup>
    <col width="5%" />

    <col width="18%" />

    <col width="10%" />

    <col width="22%" />

    <col width="45%" />
  </colgroup>

  <thead><tr><th>#</th><th>Scenario</th><th>Trigger creds</th><th>Error object</th><th>Steps to follow</th></tr></thead>

  <tbody>
    <tr><td>21</td><td>Declined by bank or risk check</td><td>`402`, `404`</td><td>on 402\
    `code` GATEWAY\_ERROR\
    `source` issuer\_bank\
    `step` payment\_debit\_request\
    `reason` payment\_declined\
    \
    on 404\
    `code` GATEWAY\_ERROR\
    `source` issuer\_bank\
    `step` payment\_debit\_response\
    `reason` payment\_risk\_check\_failed</td><td>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`.</td></tr>
    <tr><td>22</td><td>API validation errors (all methods)</td><td>Missing `amount`, `amount: 0`, wrong key secret</td><td>—</td><td>Send each bad request. Confirm HTTP 4xx, no payment created, no webhook.</td></tr>
    <tr><td>23</td><td>Invalid UPI ID (VPA resolution failed)</td><td>`401`</td><td>`code` GATEWAY\_ERROR\
    `source` network\
    `step` payment\_request\
    `reason` vpa\_resolution\_failed</td><td>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.</td></tr>
    <tr><td>24</td><td>Invalid device</td><td>`407`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` customer\
    `step` payment\_authentication\
    `reason` invalid\_device</td><td>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.</td></tr>
    <tr><td>25</td><td>Duplicate request</td><td>`406`</td><td>`code` BAD\_REQUEST\_ERROR\
    `source` gateway\
    `step` payment\_request\
    `reason` duplicate\_request</td><td>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.</td></tr>
  </tbody>
</table>

The full list of UPI error reasons, including the ones no trigger reproduces in test mode, is on the [Error index](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/error-handling.md).

### 3.4 Not available in test mode yet

| # | Scenario |
| - | - |
| 26 | Late authorisation — Live only. Handle `payment.authorized` arriving after you showed failure (up to 3 days). Check in a controlled live test. |
| 27 | Scan the QR with a real UPI app — Client-side. Scan on a real phone and approve. Check in a small live test. |
| 28 | Fund source: credit card, credit line, PPI — Coming soon. Read `payer_account_type`. Test mode always returns `bank_account`. |
| 29 | `upi.expiry_time` on the intent link behind the QR — supported in live mode, not in test mode. In live mode you can set `upi.expiry_time` on the authorisation payment to cap how long the intent link behind the QR stays valid, and the payment fails once that window closes. Test mode ignores it: the outcome is decided by the amount you send, so no amount makes the link expire and the parameter has no observable effect. Send it exactly as you would in live mode and assert that your code treats an expired QR as a terminal failure rather than something to keep polling, then check the timing itself against live traffic. |

## 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**

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Waiting for a redirect that never comes | UPI has no redirect back to you. The customer approves in their app and your page waits forever. | Drive the result from `payment.captured` / `payment.failed`, with polling as backup while the customer is in the UPI app. | 1 |
| Duplicate or unsigned events processed | One payment fulfils twice. | Verify `X-Razorpay-Signature`. Store event IDs and skip repeats. | Webhooks |
| QR left on screen after the payment ends | The customer scans an old QR again and pays a second time, which is then auto-refunded. | Replace the QR with the result as soon as you get a final status. | 9 |

**Error handling and messaging to the payer**

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Failing the payment while the customer is still in the app | You show "payment failed" while the request is still open in the UPI app, and the customer pays anyway. | Show a "complete the payment in your UPI app" state with the expiry timer. Show failure only on a final status. | 2, 3 |
| Same message for every failure | The customer retries a wrong PIN or empty account the same way. | Branch on `error.reason`: `incorrect_pin` → retry, `insufficient_funds` → use another account, bank errors → retry later. | 10, 11, 6 |
| QR shown on mobile | A customer on a phone can't scan their own screen and drops off. | Show QR on desktop. Use Intent on mobile. | 27 |
| QR not scanned in time | The QR / intent link expires and the payment fails before the customer scans it. | Regenerate the payment and QR and show it again. | 3 |

**Fetch API when webhooks are late**

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Polling stops too early | The customer approves near the end of the window and you miss it. | Poll the status URL or `GET /v1/payments/:id` until a final state or the request expiry, then keep reconciling. | 4 |
| Late authorisation ignored | A payment marked failed after 10 minutes is authorised by the bank hours later. The customer is debited and you have no order. | Listen for `payment.authorized` for 3 days. Capture and fulfil, or let it auto-refund. See [Late authorisation ↗](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/payment-states-and-late-authorisation.md). | 26 (Live only) |

**Capture & refunds**

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Not capturing an authorised payment in time | Uncaptured `authorized` payments are auto-refunded — the customer is debited and then refunded, and you treat it as a failure. | Capture within the capture window or enable auto-capture; expect `refund.processed` if you don't. | BP-14 |
| Multiple payments on the same order | More than one attempt succeeds or late-authorises on the same order, so the customer is charged twice. | Use the Orders API — it clubs attempts to one order; the duplicate or late-authorised payment auto-refunds and only one is marked paid. | BP-08, BP-09 |

## 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.

| Topic | What to know | On this method | Test it | Read more |
| - | - | - | - | - |
| Difference between order and payment | An order is what you want to collect. A payment is one attempt to pay it. One order can have many attempts but only one captured payment. Create the order on your server so the amount can't be tampered with. | Each Intent, QR or Collect request is a separate payment on the same order. | BP-24, BP-25 | [Difference between order and payment ↗](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/order-payment-relationship.md) |
| Order states | Orders move `created` → `attempted` → `paid`. An order stays `attempted` through failed attempts, and stays `paid` even after a refund. Handle Orders API errors (for example an invalid amount or currency) when you create the order. | The order stays `attempted` while the QR is on screen and unpaid. | BP-09 | [Order states ↗](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/payment-states-and-late-authorisation.md) · [Orders API error codes ↗](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/orders.md) |
| Late authorisation | A payment with no bank response is marked `failed` after 10 minutes, but Razorpay keeps polling the bank for 3 days. If the bank confirms, the payment becomes `authorized` (under 0.5% of payments). Listen for `payment.authorized` and capture it, or let it auto-refund. | Most common on UPI. A payment you showed as failed can turn `authorized` later. Live only. | BP-06 | [Late authorisation ↗](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/payment-states-and-late-authorisation.md) |
| Capture settings | Choose auto-capture or manual capture and set the timeouts. An `authorized` payment you don't capture in time is auto-refunded. You can set capture behaviour per order through the Orders API. | The auto-capture window defaults to 2 days for UPI. | BP-14 | [Capture settings ↗](https://razorpay.com/docs/payments/payment-gateway/rainy-day/capture-settings) |
| Multiple payments on the same order | Once an order is `paid`, no more payments are accepted on it. While a payment on the order is `authorized`, a new attempt isn't allowed. An attempt that authorises late on a paid order is auto-refunded. Retry on the same order so a customer can't pay twice. | A customer can scan an old QR and pay twice. Only one payment captures; the other is auto-refunded. Replace the QR once the order is paid. | BP-09 · BP-08 | [Multiple payments on the same order ↗](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/payment-states-and-late-authorisation.md) |

All best practices: [Overview](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices) · [Webhooks and Fetch APIs](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/webhooks-and-fetch-apis.md) · [Payment states and late authorisation](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/payment-states-and-late-authorisation.md) · [Error handling](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/error-handling.md) · [Downtime handling](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/downtime-handling.md) · [Security](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices/security.md)

### Related Information

* [UPI Intent (One-time)](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/upi-intent.md)
* [UPI Collect (One-time)](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/upi-collect.md)
* [Cards (One-time)](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/cards)
* [Orders](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/orders.md)
* [Refunds](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/refunds.md)
* [Best Practices](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/best-practices)
