POSTGenerate UPI QR for 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.
- Moves money: Moves real money and cannot be undone. After a timeout, confirm the outcome before ever re-sending.
- Earns commission: You earn 0.40% up to ₹3,000; ₹13 flat for ₹3,001–₹5,000 per withdrawal.
https://staging.eko.in/ekoapi/v3 return an error response. Test it in Production.Lets a retailer dispense cash to a customer who pays over UPI. You request a withdrawal for an amount; the API returns a UPI QR valid for about 60 seconds. The customer scans it from any UPI app and authorises with their own UPI PIN. Once the payment succeeds the retailer's wallet is credited (amount + commission − TDS) and the retailer hands over the cash.
It is a two-step flow:
- Generate QR (this API) — send the amount and location, receive a QR image
and a transaction id (
tid). - Status — poll with that
tidfor the final outcome.
The customer's payment settles asynchronously. A successful response here
does not mean the customer has paid — tx_status is PENDING and no money
has moved. Hand over cash only when the Status API returns tx_status "0".
Key facts
- Amount: ₹100 – ₹5,000, in multiples of ₹100, whole rupees only (no paise). Validate in your app before calling.
- Daily caps: ₹10,000 / ₹30,000 per merchant per day, set by the acquiring bank.
- Blocked window: no transactions between 23:30 and 00:05 IST.
- Caps and the window are NPCI / acquirer rules, not Eko's.
- No biometric, Aadhaar or daily eKYC for the customer — they authorise with their UPI PIN.
- Retailer prerequisite: the retailer must be onboarded and eKYC-complete for
UPI Cash Withdrawal. Until then the API returns
response_type_id99016. - Location: send a real, current GPS fix in decimal degrees. Geo-fencing is enforced; zeros, stale values and coarse network locations are rejected.
- Sandbox: not yet available on UAT.
Showing the QR
qr_string is a base64-encoded PNG, not a upi://pay deep link. Render it
as an image:
<img src="data:image/png;base64,{qr_string}" alt="UPI QR" />
The customer scans it from a second device — there is no same-device app
handoff. Show qr_expiry_note next to it and a countdown from qr_expiry_in.
Each QR is single-use and amount-specific: never show the same QR twice and
never re-initiate against the same tid.
Polling the status
The response tells you when to poll the Status API:
- First call after
status_check_start_inseconds. - Then every
status_check_gapseconds. - Up to
status_check_attemptstimes.
If it is still pending after the last scheduled poll, stop, treat it as not paid, and re-query once after a few minutes.
If this call times out
If you get no response (and so no tid), the customer has not seen a QR and
cannot have paid against it. Wait for the QR validity window (~60 seconds) to
pass, then start a new transaction.
Why an initiation fails
A 99014 response carries the acquirer's reason in data.description. Log it
and show it to the retailer — message is generic. The list below is not
exhaustive; always surface the field you actually received.
| Reason | What it means and what to do |
|---|---|
| QR not scanned | The QR expired before the customer paid. Normal. Start a new transaction. |
| Account is being validated | The retailer's onboarding is still in progress at the acquirer. Retry later; it is not a request error. |
| Merchant's daily transaction limit exceeded | The ₹10,000 / ₹30,000 per-day cap is hit. No further transactions until the next day. |
| Transaction restricted from 23:30 to 00:05 | Acquirer maintenance window. Retry after 00:05 IST. |
| Please complete eKYC before initiating | The retailer's product eKYC is incomplete. A provisioning task, not a retry. |
| Invalid latitude/longitude · geo-fencing invalid · service restricted in this area | Send a real GPS fix as decimal degrees. Zeros, stale values and coarse network locations are rejected. |
| Amount exceeded limit · less than minimum · not a multiple of ₹100 · paise not allowed | Validate the amount in your app before calling. |
| Account blocked · agent in NPCI negative registry | The retailer cannot transact. Route to your distributor; retrying will not help. |
| Temporarily suspended / unable to process | An upstream outage. Retry with backoff; if it persists, raise it with Eko support. |
- UPI Cash Withdrawal — Generate a single-use UPI QR for the withdrawal amount, show it to the customer, then poll the status API on the returned schedule — hand over cash only when `tx_status` is "0".
Request
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
initiator_id | string | required | Registered mobile number of the API user (see Platform Credentials).e.g. 9962981729 |
user_code | string | required | 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 | required | 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 | required | 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 | required | Retailer device longitude in decimal degrees.e.g. 77.2090 |
customer_mobile | string | optional | Customer's 10-digit mobile number.e.g. 9000000000 |
initiator_idstringRequiredRegistered mobile number of the API user (see Platform Credentials).
example: 9962981729
user_codestringRequiredUnique code of the retailer the withdrawal is run for. The retailer must be onboarded and eKYC-complete for UPI Cash Withdrawal.
example: 34127001
amountstringRequiredWithdrawal 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.
example: 100
latitudestringRequiredRetailer 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.
example: 28.6139
longitudestringRequiredRetailer device longitude in decimal degrees.
example: 77.2090
customer_mobilestringoptionalCustomer's 10-digit mobile number.
example: 9000000000
These headers authenticate and sign every request. See How Auth Works for details.
| Field | Type | Required | Description |
|---|---|---|---|
developer_key | string | required | Static API key issued to your account after KYC. |
secret-key | string | required | Dynamic per-request signature: base64(HMAC-SHA256(timestamp, base64(access_key))). |
secret-key-timestamp | string | required | Current time in milliseconds since UNIX epoch, used to compute secret-key. Must match server time. |
content-type | string | required | application/jsone.g. application/json |
developer_keystringRequiredStatic API key issued to your account after KYC.
secret-keystringRequiredDynamic per-request signature: base64(HMAC-SHA256(timestamp, base64(access_key))).
secret-key-timestampstringRequiredCurrent time in milliseconds since UNIX epoch, used to compute secret-key. Must match server time.
content-typestringRequiredapplication/json
example: application/json
Response types
| 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 |
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 | — |
Responses
99012QR generated — show `qr_string` to the customer. No money has moved yet; poll the status API with `tid`Next:UPI Cash Withdrawal Status
statusnumberPrimary success indicator (0 = success).
messagestringHuman-readable response / error message.
response_status_idnumberUI 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_idnumberA unique id for every possible response shape (success or error) — useful for client logic branching and analytics.
dataobjectAPI-specific response payload.
tidstringEko transaction id. Send it to the Status API and quote it in any support request.
qr_stringstringBase64-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.
tx_statusstring`PENDING` on this response: a QR has been issued, no money has moved. The payment outcome comes only from the Status API.
amountstringWithdrawal amount (₹).
commissionstringRetailer commission (₹) credited with the amount once the payment succeeds.
tdsstringTDS (₹) deducted from the commission.
qr_expiry_instringQR validity in seconds.
qr_expiry_notestringDisplay note to show the retailer alongside the QR.
qr_expiry_datestringAbsolute QR expiry timestamp.
status_check_start_instringSeconds to wait before the first Status call.
status_check_gapstringSeconds between subsequent Status calls.
status_check_attemptsstringMaximum number of scheduled Status calls. Still pending after the last one → treat as not paid and re-query once after a few minutes.
fp_transaction_idstringAcquiring-platform reference.
vpastringThe collecting VPA.
user_codestringEcho of the retailer's `user_code`.
timestampstringResponse timestamp (DD-MM-YY HH:MM:SS).
descriptionstringOnly 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.