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

  1. 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.
  2. 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).
  3. 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"}
  }'
FieldRequiredNotes
amountyesRupees, up to 2 decimals. Your package limits apply in live mode.
order_idrecommendedYour 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.
descriptionnoShown to the customer (255 characters).
customer_name / _phone / _emailnoSome providers show or require them (Cashfree needs a phone).
customer_ipnoThe shopper’s IP, checked against the risk blocklist.
redirect_urlnoWhere the customer returns after paying. Defaults to the return URL in your business profile.
provider / connector_idnoForce a provider (phonepe, paytm, razorpay, cashfree, upi, sandbox) or one of your UPI accounts. Default: your default account for the key’s mode.
expiry_minutesno5 to 1440. Default 15.
udfnoUp 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:

createdWaiting for the customer.
pendingThe customer acted; the bank or the merchant (Direct UPI UTR) still has to confirm.
successPaid. Fulfil the order.
failedDeclined, cancelled or rejected.
expiredNot paid in time.
refundedFully refunded.
partially_refundedPart of it refunded.

Other endpoints

CallWhat it does
GET /pingChecks 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}/utrDirect UPI: report the UTR your customer gave you (utr, optional payer_vpa).
POST /orders/{id}/refundsRefund all or part (amount, reason).
GET /refunds/{id}Refund status.
POST /linksCreate 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 /balanceWallet 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.

Chat with us
Hi! Send us a message and we will reply here as soon as we can.