---
title: "Generate UPI QR for Cash Withdrawal API Reference"
description: "Generate a single-use UPI QR for a cash withdrawal — the customer scans it from any UPI app and pays with their own UPI PIN, then you poll the status API before handing over cash."
canonical: "https://eps.eko.in/docs/upi-cash-withdrawal"
---


> **Canonical URL:** https://eps.eko.in/docs/upi-cash-withdrawal
> This is a machine-readable Markdown version of the page for AI agents and LLMs. The primary (HTML) version lives at the canonical URL above.

# Generate UPI QR for Cash Withdrawal API Reference

`POST https://staging.eko.in/ekoapi/v3/customer/payment/upi-cash-withdrawal`

Generate a single-use UPI QR for a cash withdrawal — the customer scans it from any UPI app and pays with their own UPI PIN, then you poll the status API before handing over cash.

**Behaviour**

- **Moves money**: Moves real money and cannot be undone. After a timeout, confirm the outcome before ever re-sending.
- **[Earns commission](https://eps.eko.in/pricing?tab=payments&pay=upi-cash-withdrawal:1000:2000#payments-calculator)**: You earn 0.40% up to ₹3,000; ₹13 flat for ₹3,001–₹5,000 per withdrawal.

> [!WARNING]
> The UAT / Sandbox environment is not available for this API — requests to `https://staging.eko.in/ekoapi/v3` return an error response. Test it in Production.

Step 1 of 2. Send the withdrawal amount (₹100–₹5,000, multiples of ₹100, no paise) and the retailer's live GPS fix; get back a base64 PNG UPI QR (`qr_string`, valid ~60 seconds), a transaction id (`tid`) and a polling schedule. The customer scans the QR from a second device with any UPI app and authorises with their own UPI PIN — no biometric, Aadhaar or customer eKYC. The retailer must already be onboarded and eKYC-complete for UPI Cash Withdrawal (else `response_type_id` 99016).

**A successful response only means a QR was issued — no money has moved (`tx_status` `PENDING`).** Poll [UPI Cash Withdrawal Status](/docs/upi-cash-withdrawal-status) with `tid`: first after `status_check_start_in` seconds, then every `status_check_gap` seconds, up to `status_check_attempts` times. **Hand over cash only when Status returns `data.tx_status` `"0"`**; anything else — failed, pending, unknown, timeout — means not paid. Never show a QR twice or re-initiate against the same `tid`.

If this call times out with no `tid`, the customer never saw a QR and cannot have paid against it: wait for the QR validity window to pass, then start a new transaction.

Per-merchant daily caps (₹10,000 / ₹30,000, set by the acquiring bank) apply, and transactions are blocked between 23:30 and 00:05 IST (NPCI / acquirer rules).

> View product & pricing details: [UPI Cash Withdrawal](https://eps.eko.in/products/upi-cash-withdrawal-api.md)

## Body parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| initiator_id | string | yes | Registered mobile number of the API user (see Platform Credentials). e.g. 9962981729 |
| user_code | string | yes | Unique code of the retailer the withdrawal is run for. The retailer must be onboarded and eKYC-complete for UPI Cash Withdrawal. e.g. 34127001 |
| amount | string | yes | Withdrawal amount in ₹, as a whole-rupee string: ₹100–₹5,000 in multiples of ₹100, no paise (e.g. `"100"`, `"2500"`). Validate before calling — an invalid amount returns `response_type_id` 99015. e.g. 100 |
| latitude | string | yes | Retailer device latitude in decimal degrees. Must be a real, current GPS fix — geo-fencing is enforced; zeros, stale values and coarse network locations are rejected. e.g. 28.6139 |
| longitude | string | yes | Retailer device longitude in decimal degrees. e.g. 77.2090 |
| customer_mobile | string | no | Customer's 10-digit mobile number. e.g. 9000000000 |

## Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| developer_key | string | yes | Static API key issued to your account after KYC. |
| secret-key | string | yes | Dynamic per-request signature: base64(HMAC-SHA256(timestamp, base64(access_key))). |
| secret-key-timestamp | string | yes | Current time in milliseconds since UNIX epoch, used to compute secret-key. Must match server time. |
| content-type | string | yes | application/json e.g. application/json |

## Response

⭐ marks fields highlighted as verifiable.

| Field | Type | Description |
| --- | --- | --- |
| status | number | Primary success indicator (0 = success). |
| message | string | Human-readable response / error message. |
| response_status_id | number | UI display hint only (-1/0 success, 1 failure, 2 initiated, 3 refund initiated, 4 refunded). Never branch on it — use `status` (and `tx_status` for financial APIs), or `response_type_id` where documented. |
| response_type_id | number | A unique id for every possible response shape (success or error) — useful for client logic branching and analytics. |
| data | object | API-specific response payload. |
| data.tid ⭐ | string | Eko transaction id. Send it to the Status API and quote it in any support request. |
| data.qr_string ⭐ | string | Base64-encoded PNG of the UPI QR — render it as `data:image/png;base64,<qr_string>`. Not a `upi://pay` deep link: the customer scans it from a second device. Real values are several KB (the sample is a 1×1 placeholder). Single-use and amount-specific. |
| data.tx_status ⭐ | string | `PENDING` on this response: a QR has been issued, no money has moved. The payment outcome comes only from the Status API. |
| data.amount ⭐ | string | Withdrawal amount (₹). |
| data.commission | string | Retailer commission (₹) credited with the amount once the payment succeeds. |
| data.tds | string | TDS (₹) deducted from the commission. |
| data.qr_expiry_in ⭐ | string | QR validity in seconds. |
| data.qr_expiry_note | string | Display note to show the retailer alongside the QR. |
| data.qr_expiry_date | string | Absolute QR expiry timestamp. |
| data.status_check_start_in ⭐ | string | Seconds to wait before the first Status call. |
| data.status_check_gap ⭐ | string | Seconds between subsequent Status calls. |
| data.status_check_attempts ⭐ | string | Maximum number of scheduled Status calls. Still pending after the last one → treat as not paid and re-query once after a few minutes. |
| data.fp_transaction_id | string | Acquiring-platform reference. |
| data.vpa | string | The collecting VPA. |
| data.user_code | string | Echo of the retailer's `user_code`. |
| data.timestamp | string | Response timestamp (DD-MM-YY HH:MM:SS). |
| data.description | string | Only on a failed initiation (`response_type_id` 99014): the failure reason from the acquiring platform. Log it and show it to the retailer — `message` is generic. |

## Response types

Branch on `response_type_id` to decide the next call:

| response_type_id | Meaning | Next step |
| --- | --- | --- |
| 99012 | QR generated — show `qr_string` to the customer. No money has moved yet; poll the status API with `tid` | [upi-cash-withdrawal-status](https://eps.eko.in/docs/upi-cash-withdrawal-status.md) |
| 99014 | Initiation failed — nothing to poll. Log `data.description`; start a new transaction only if the reason is retryable | — |
| 99015 | Invalid amount — must be ₹100–₹5,000 in multiples of ₹100, no paise. Fix the amount; do not retry as-is | — |
| 99016 | Not enabled for this account — a provisioning state; retrying will not help. Contact your relationship manager | — |

## Example request

```json
{
  "initiator_id": "9962981729",
  "user_code": "34127001",
  "amount": "100",
  "latitude": "28.6139",
  "longitude": "77.2090",
  "customer_mobile": "9000000000"
}
```

## Example response

`response_type_id` `99012` — QR generated — show `qr_string` to the customer. No money has moved yet; poll the status API with `tid`. Next step: [upi-cash-withdrawal-status](https://eps.eko.in/docs/upi-cash-withdrawal-status.md).

```json
{
  "response_status_id": 0,
  "response_type_id": 99012,
  "status": 0,
  "message": "UPI QR generated. Ask the customer to scan and pay.",
  "data": {
    "tid": "123456789",
    "amount": "100.0",
    "commission": "0.42",
    "tds": "0.0",
    "tx_status": "PENDING",
    "qr_string": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
    "fp_transaction_id": "FP-XXXXXXXX",
    "vpa": "example@bankvpa",
    "qr_expiry_in": "60",
    "qr_expiry_note": "Ask the customer to scan and pay within the time shown",
    "qr_expiry_date": "2026-11-05T22:41:27",
    "status_check_start_in": "60",
    "status_check_gap": "30",
    "status_check_attempts": "2",
    "user_code": "34127001",
    "timestamp": "05-11-26 22:40:27"
  }
}
```

## Error scenarios

| Status | response_type_id | Scenario |
| --- | --- | --- |
| 200 | `99015` — Invalid amount — must be ₹100–₹5,000 in multiples of ₹100, no paise. Fix the amount; do not retry as-is | Invalid amount — outside ₹100–₹5,000, not a multiple of ₹100, or has paise |
| 200 | `99016` — Not enabled for this account — a provisioning state; retrying will not help. Contact your relationship manager | Not enabled for this account — the retailer is not yet onboarded for UPI Cash Withdrawal. A provisioning state, not a request error: retrying will not clear it; raise it with your relationship manager |
| 200 | `99014` — Initiation failed — nothing to poll. Log `data.description`; start a new transaction only if the reason is retryable | Initiation failed — `data.description` carries the acquirer's reason (QR not scanned, daily limit exceeded, 23:30–00:05 window, eKYC incomplete, invalid location, account blocked, upstream outage…). Log and show `description`, not the generic `message` |
