> ## 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 Collect: How to Test Your Integration

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

Available in
🇮🇳 India

> [!WARNING]
>
> Collect is deprecated from 28 February 2026. Exemptions: specific MCCs, iOS and mobile web, UPI Mandates, eRupi vouchers and PACB businesses. Everyone else should build and test Intent (mobile) and QR (desktop).

Send collect requests, simulate failures and reconcile against the webhooks in test mode, then check VPA validation and the approval window on live traffic before you go live.

This page covers one-time UPI Collect payments on S2S: you send a payment request to the customer's UPI ID and they approve it inside their UPI app.

**UPI flows:** [Intent](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/upi-intent.md) · [QR](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/upi-qr.md) · Collect

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 | A collect request lands in the customer's UPI app and they approve it there with their UPI PIN, within the expiry window you set. | The simulator approves or declines on its own. No request reaches a UPI app, and the outcome is decided by the VPA and the amount you send rather than by anything the customer does. |
| Collect request | Razorpay sends the request to the VPA you pass. It shows up in the customer's UPI app as a pending request carrying your business name and the amount. | Never sent. The payment response carries only a payment ID, followed by the relevant webhooks. Send `success@razorpay` to have it approved and `failure@razorpay` with a reserved amount to have it declined. |
| Validate VPA | `POST /v1/payments/validate/vpa` resolves the UPI ID against the customer's PSP before you send the collect request, and returns the name on the account. | Coming soon. You cannot exercise the validate-then-send sequence here yet. Build it anyway, since a collect request sent to a VPA that does not resolve fails without reaching anyone. |
| Saved VPA | Pass `save` on the payment to store the customer's UPI ID so they do not have to type it again next time. | Coming soon. Saving and reusing a VPA is not simulated yet, so check it on live traffic. |
| 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 collect 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. |
| Validate VPA and saved VPA | Neither is simulated yet. Build the validate-then-send sequence anyway, and verify both against live traffic. |

### 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 |
| Validate a VPA | `POST /v1/payments/validate/vpa` | Coming soon |
| 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 | `payment.failed` | Available |
| 5 Full and partial refund | `refund.processed` | Available |
| 11 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 `success@razorpay` as the VPA with `100` (₹1) on both the order and the payment to succeed.
* Send `failure@razorpay` with a reserved amount from [3.3 UPI error scenarios](#3-3-upi-error-scenarios) to fail with the error mapped to that amount.
* No collect request reaches a UPI app, so there is nothing to approve.
* 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 Collect 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.

**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>`success@razorpay` + `amount: 100`</td><td>Create an order with the trigger amount. Create the payment: `POST /v1/payments/create/upi` with `upi.flow: "collect"` and `upi.vpa`. Show a "complete the payment in your UPI app" state with a timer. 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</td><td>`failure@razorpay` + `302`</td><td>Create an order with the trigger amount. Create the payment: `POST /v1/payments/create/upi` with `upi.flow: "collect"` and `upi.vpa`. Show a "complete the payment in your UPI app" state with a timer. Confirm `payment.failed` with `payment_cancelled`. The order stays open.</td></tr>
    <tr><td>3</td><td>Request expires</td><td>`failure@razorpay` + `301`</td><td>Create an order with the trigger amount. Create the payment: `POST /v1/payments/create/upi` with `upi.flow: "collect"` and `upi.vpa`. Show a "complete the payment in your UPI app" state with a timer. Confirm `payment_collect_request_expired`. 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 `failure@razorpay` and each reserved amount to cover the error scenarios in 3.3, and show the expiry timer so the customer knows how long they have to approve in their UPI app.</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>`failure@razorpay` + `104`</td><td>`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: "collect"` and `upi.vpa`. Show a "complete the payment in your UPI app" state with a timer. Confirm `bank_technical_error` and a retry option.</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>7</td><td>Insufficient funds</td><td>`failure@razorpay` + `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: "collect"` and `upi.vpa`. Show a "complete the payment in your UPI app" state with a timer. Confirm `insufficient_funds` and a "use another account" message.</td></tr>
    <tr><td>8</td><td>Wrong UPI PIN</td><td>`failure@razorpay` + `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: "collect"` and `upi.vpa`. Show a "complete the payment in your UPI app" state with a timer. Confirm `incorrect_pin` and a retry option.</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>9</td><td>Declined by bank or risk check</td><td>`failure@razorpay` + `404`</td><td>`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: "collect"` and `upi.vpa`. Show a "complete the payment in your UPI app" state with a timer. Confirm `payment_risk_check_failed`.</td></tr>
    <tr><td>10</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>
  </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 |
| - | - |
| 11 | Late authorisation — Live only. Handle `payment.authorized` arriving after you showed failure (up to 3 days). Check in a controlled live test. |
| 12 | Invalid UPI ID entered — Coming soon. Handle `invalid_vpa` from the API. Check live. |
| 13 | Validate VPA before sending — Coming soon. Build to spec (`POST /v1/payments/validate/vpa`). Check live. |
| 14 | Save and reuse a VPA — Coming soon. Build to spec. Check live. |
| 15 | Fund source: credit card, credit line, PPI — Coming soon. Read `payer_account_type`. Test mode always returns `bank_account`. |

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

**Before you send — validate the VPA (Collect only)**

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Not validating the VPA before sending | Collect requests fire at mistyped or non-existent UPI IDs, so they fail only after the full expiry wait. | Call `POST /v1/payments/validate/vpa` first and send the collect only if it resolves. | 13 |
| Branching on HTTP status instead of the `success` field | `success: false` is returned as an HTTP 200, so a valid "VPA does not exist" is mistaken for a server error. `@paytm` handles also return 200 with `success: false`. | Branch on the `success` boolean, not the HTTP status; `customer_name` appears only when `success: true`; treat `@paytm` as a negative result, not an error. | 12 |

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

**Saved VPA (Collect only)**

| Gap | What goes wrong in production | What to do | Test with |
| - | - | - | - |
| Saved VPA not refreshed | A customer's saved UPI ID stops working and every Collect fails. | Refresh saved methods from the API before showing them. | 14 |
| Treating a saved VPA as proven-good | The VPA is saved when the payment is created (before the bank replies), so it is saved even if that payment later failed. | Don't assume a saved VPA has ever succeeded; confirm it on the first successful payment. | 14 |
| Showing duplicate saved UPI IDs | Every `save: 1` payment creates a new saved token, so the same customer + VPA can appear in the saved list several times. | De-duplicate by VPA before showing the saved list. | 14 |

**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. | 7, 8, 6 |
| Collect shown to customers who can't use it | After 28 Feb 2026, non-exempt Collect requests fail. | Show Intent and QR. Keep Collect only if you are exempt. | — |
| No expiry timer | The customer doesn't know how long they have to approve, and the request expires. | Show the time left and a "resend request" option after expiry. | 3 |
| Resending a collect while one is still pending | The customer gets multiple collect prompts and may approve more than one, risking a double debit that is later reconciled or refunded. | Allow a resend only after the current request expires; show the expiry timer meanwhile. | 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). | 11 (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 Collect request is pending approval. | 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 get more than one Collect request and approve both. Only one payment captures; the other is auto-refunded. | 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 QR (One-time)](https://razorpay.com/docs/payments/payment-gateway/s2s-integration/test-s2s/one-time/upi-qr.md)
* [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)
