API documentation
Create orders, send customers to the hosted UPI checkout, and get signed webhooks. Build in staging, then switch to your live key.
Quick start
- Create a staging API key (it starts with sk_test_). Staging payments use your staging UPI account or the built-in simulator, have no transaction charges and do not count towards your limits.
- Create an order from your server, send the customer to the payment_url you get back, and wait for the webhook (or check the status).
- When it works, connect a live UPI account (PhonePe is the default), create a live key (sk_live_) and switch your server to it.
Base URL: https://www.upipayments.in/api/v1 · Send the key as Authorization: Bearer sk_… · JSON in, JSON out. Never put a secret key in an app or a web page.
Create an order
curl -X POST https://www.upipayments.in/api/v1/orders \
-H "Authorization: Bearer sk_test_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 499.00,
"order_id": "ORD-10042",
"description": "Premium plan",
"customer_name": "Priya Sharma",
"customer_phone": "9876543210",
"customer_email": "[email protected]",
"redirect_url": "https://shop.demo.pepploweb.com/thank-you",
"udf": {"cart": "C-77"}
}'
| Field | Required | Notes |
|---|---|---|
| amount | yes | Rupees, up to 2 decimals. Your package limits apply in live mode. |
| order_id | recommended | Your own id, unique per mode. Sending the same order_id and amount again returns the same payment (safe retries); a different amount is refused with duplicate_order_id. |
| description | no | Shown to the customer (255 characters). |
| customer_name / _phone / _email | no | Some providers show or require them (Cashfree needs a phone). |
| customer_ip | no | The shopper’s IP, checked against the risk blocklist. |
| redirect_url | no | Where the customer returns after paying. Defaults to the return URL in your business profile. |
| provider / connector_id | no | Force a provider (phonepe, paytm, razorpay, cashfree, upi, sandbox) or one of your UPI accounts. Default: your default account for the key’s mode. |
| expiry_minutes | no | 5 to 1440. Default 15. |
| udf | no | Up to 5 of your own key/value pairs, returned in every response and webhook. |
Reply (201):
{
"success": true,
"data": {
"id": "pay_k3j9x0a1b2c3d4e5f6g7",
"order_id": "ORD-10042",
"status": "created",
"mode": "staging",
"amount": 499,
"currency": "INR",
"provider": "phonepe",
"fees": {"charges": 0, "gst": 0, "total": 0},
"payment_url": "https://www.upipayments.in/checkout/…",
"upi_intent": "upi://pay?pa=…", // Direct UPI only: show your own QR
"qr_url": "https://www.upipayments.in/checkout/…/qr.svg",
"expires_at": "2026-10-16T12:45:00+00:00",
…
}
}
Check a payment
curl https://www.upipayments.in/api/v1/orders/pay_k3j9x0a1b2c3d4e5f6g7 -H "Authorization: Bearer sk_test_xxxxxxxx"
# or by your own order id
curl https://www.upipayments.in/api/v1/orders/ORD-10042 -H "Authorization: Bearer sk_test_xxxxxxxx"
While a payment is open, this call asks the provider for the latest state first. Status values:
| created | Waiting for the customer. |
| pending | The customer acted; the bank or the merchant (Direct UPI UTR) still has to confirm. |
| success | Paid. Fulfil the order. |
| failed | Declined, cancelled or rejected. |
| expired | Not paid in time. |
| refunded | Fully refunded. |
| partially_refunded | Part of it refunded. |
Other endpoints
| Call | What it does |
|---|---|
| GET /ping | Checks the key; tells you whether live payments are allowed yet. |
| GET /orders?status=&from=&to=&per_page= | Lists payments of the key’s mode, newest first. |
| POST /orders/{id}/utr | Direct UPI: report the UTR your customer gave you (utr, optional payer_vpa). |
| POST /orders/{id}/refunds | Refund all or part (amount, reason). |
| GET /refunds/{id} | Refund status. |
| POST /links | Create a payment link (title, optional amount, reusable, expires_at, max_uses, customer fields). |
| GET /links/{id} | A link with its uses and the amount collected. |
| GET /balance | Wallet balance, package, transaction charge and GST rates, limits and today’s / this month’s usage. |
Only GET and POST are used. Errors look like {"success": false, "error": {"code": "daily_count_limit", "message": "…"}} with HTTP 401 (key), 402 (wallet too low for the fee), 403 (not allowed), 404, 409 (duplicate order id / no UPI account), 422 (validation), 429 (package limit or rate limit) or 502 (provider).
Webhooks
Add your URL under Developers › Webhooks. Each event is a POST with a JSON body:
{"id": "evt_…", "event": "payment.success", "mode": "live", "created_at": "…", "data": {"payment": { … same fields as the API … }}}
Verify it before you trust it (PHP):
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_UPI_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_UPI_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, getenv('UPI_SIGNING_SECRET'));
if (! hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }
$event = json_decode($body, true);
if ($event['event'] === 'payment.success') { /* mark order $event['data']['payment']['order_id'] paid (once) */ }
http_response_code(200);
Events: payment.success, payment.failed, payment.expired, payment.pending, refund.processed, refund.failed. Reply with any 2xx within 8 seconds. Webhooks can arrive more than once and out of order: make your handler idempotent.
Return to your website
After paying, the customer is sent to your redirect_url with payment_id, order_id, status, amount and signature = HMAC-SHA256(payment_id|order_id|status|amount, signing secret). Use it to show a message, but fulfil orders from the webhook or a status check.
PHP SDK
require 'UpiPaymentsClient.php';
$upi = new UpiPaymentsClient('sk_test_xxxxxxxx', 'https://www.upipayments.in');
$order = $upi->createOrder(['amount' => 499, 'order_id' => 'ORD-10042', 'redirect_url' => 'https://shop.demo.pepploweb.com/thank-you']);
header('Location: ' . $order['payment_url']);
$payment = $upi->getOrder('ORD-10042'); // status check
$ok = UpiPaymentsClient::verifyWebhook($body, $ts, $sig, $signingSecret);
Direct UPI (your own QR)
With a Direct UPI account, the order reply includes upi_intent and qr_url. Show the QR in your app or on your counter screen; the customer pays the exact amount and you confirm the UTR in the panel (or send it to POST /orders/{id}/utr and confirm it there). Nothing is confirmed automatically, because a UPI ID alone has no status API.