Base URL and authentication
All endpoints are under https://cashvya.com and return JSON. Amounts are
accepted in rupees and returned both in rupees (amount) and paise
(amountPaise), so you never have to guess the unit.
Server-to-server
Send your secret key either way:
Authorization: Bearer cashvya_live_YOUR_KEY
X-Api-Key: cashvya_live_YOUR_KEY
Browser
A browser cannot hold a secret. The SDK identifies you with your public merchant
id in X-Merchant-Id. A browser-created payment is less trusted: redirect
URLs are restricted to origins you own, and you can switch browser-created payments off entirely
in your dashboard, which makes POST /api/v1/payments return
403 BROWSER_CHECKOUT_DISABLED for anything but your own server.
If you send a cashvya_live_ key that has been regenerated or revoked, the
request returns 401 INVALID_API_KEY. It does not silently fall back to an
untrusted call.
POST /api/v1/payments
Creates a payment session and returns the checkout URL.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
orderId | string | Yes | Your reference. Up to 128 characters, no control characters. Unique per order. |
amount | string / number | Yes | Rupees, max 2 decimals, e.g. "499.00" |
currency | string | No | Defaults to INR |
method | string | No | Defaults to UPI. Also CASH_COUNTER, CARD_COUNTER for in-person. |
customer | object | No | fullName, email, phoneNumber. Shown to you on the payment and in the webhook. |
returnUrl | string | No | Where to send the customer after success. Falls back to your dashboard setting. |
cancelUrl | string | No | Where to send the customer if they cancel. |
metadata | object | No | Returned to you unchanged in the webhook. Gateway-reserved keys are stripped. |
idempotencyKey | string | No | Or send the Idempotency-Key header. |
Responses
201 Created — a new payment:
{
"paymentId": "7f3c1e62-...",
"orderId": "ORDER-1042",
"amount": "499.00",
"amountPaise": "49900",
"currency": "INR",
"status": "INITIATED",
"queuePosition": 0,
"checkoutUrl": "https://cashvya.com/checkout/7f3c1e62-..."
}
200 OK — the same orderId already exists. This is what makes retries
safe:
- Already paid: the original payment is returned with
"status": "SUCCESS". Do not charge again. - Still open: the existing payment is returned with
"activeSessionDetected": true, so you can resume the same checkout instead of creating a second one. - Previously failed or expired: a fresh payment is created for the same
orderId.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | — | orderId or amount missing, or the amount is not a valid rupee value |
| 400 | INVALID_ORDER_ID | Over 128 characters or contains control characters |
| 401 | INVALID_API_KEY | Key revoked, regenerated or mistyped |
| 401 | INVALID_MERCHANT_KEY | X-Merchant-Key did not match your webhook secret |
| 403 | MERCHANT_SUSPENDED | Your account is suspended and cannot take payments |
| 403 | BROWSER_CHECKOUT_DISABLED | You require server-created payments; create it with your secret key |
| 409 | DEVICE_SESSION_MISMATCH | That payment was started on a different device |
| 503 | UPI_UNAVAILABLE | No receiving UPI account is available right now. Retry shortly. |
GET /api/v1/payments/:paymentId
Reads a payment. The response depends on whether you prove ownership.
Without ownership
Anyone holding the payment id gets the public view only:
{
"paymentId": "7f3c1e62-...",
"merchantId": "00000000-...",
"pageSlug": null,
"orderId": "ORDER-1042",
"amount": "499.00",
"amountPaise": "49900",
"currency": "INR",
"status": "SUCCESS",
"method": "UPI",
"completedAt": "2026-10-31T06:12:40.112Z"
}
With ownership
Send X-Merchant-Key: <your whsec_… webhook secret>, or your admin
credential, to additionally receive:
{
"internalStatus": "SUCCESS",
"verificationLevel": 2,
"verificationSources": ["SMS", "GMAIL"],
"verificationTag": "BANK_VERIFIED",
"returnUrl": "https://yourshop.com/thank-you",
"metadata": { "...": "your metadata, plus the customer summary" }
}
404 if the payment id does not exist.
GET /api/v1/checkout/:paymentId/state
The live state the checkout page itself polls. Useful if you build your own
payment screen. Includes status, timeRemainingSeconds,
expiresAt, serverNow, upiUri, payeeVpa,
payeeName, enabledMethods, providers and the merchant's
public branding.
A terminal payment returns "isTerminal": true with
timeRemainingSeconds: 0.
This endpoint exists to render a screen. For fulfilment, use the webhook or
GET /api/v1/payments/:id with your key.
Payment statuses
| Status | Terminal | Meaning |
|---|---|---|
INITIATED | No | Created. The customer has not started paying, and no timer is running yet. |
QUEUED | No | Waiting for a receiving UPI slot to free up. |
ACTIVE | No | Holds a slot. The customer has about 5 minutes to pay. |
PROCESSING | No | The customer says they paid; Cashvya is matching it against bank notifications. |
SUCCESS | Yes | A bank credit was matched to this payment. Safe to fulfil. |
FAILED | Yes | No matching credit was found within the verification window. |
EXPIRED | Yes | The payment window elapsed without payment. |
CANCELLED | Yes | The customer abandoned the checkout. |
BLOCKED | Yes | Stopped by fraud controls. |
Webhooks are sent for the terminal outcomes only.
Key management
These are dashboard endpoints, authenticated with your dashboard session.
| Method and path | Does |
|---|---|
GET /api/merchant/keys | Masked status of your key and webhook secret |
POST /api/merchant/keys | Generate a key. Returns it once and revokes the previous one. |
DELETE /api/merchant/keys | Revoke the key. Integrations using it stop immediately. |
POST /api/merchant/webhook-secret | Rotate the webhook signing secret |
POST /api/merchant/keys/reveal | Show a stored secret again. Requires { password, what }. |
Generating a new key invalidates the old one at once, and rotating the webhook secret means deliveries are signed with the new secret from that moment. Deploy the new value first.
Error format
Errors are JSON with a human message and, where it helps you branch, a stable
code:
{ "error": "This merchant account is suspended and cannot accept payments.",
"code": "MERCHANT_SUSPENDED" }
Branch on code, never on the text of error.