PayUPI API documentation
PayUPI is a self-serve UPI collection gateway. Create an order from your server, send the customer to the hosted checkout page, and the order is confirmed automatically — no commission, no reconciliation, money straight into your UPI account.
https://payupi.co/api/public/v1application/jsonX-API-KeyQuickstart
- Create an account and sign in to the merchant console.
- Connect an account — pick PhonePe or Paytm and complete the setup on the Connect Accounts page.
- Generate an API key on the API Keys page. Copy it once; regenerating replaces the old key.
- Create an order from your server and redirect the customer to the returned
payment_url. - Set your URLs on the Config page — success redirect, failed redirect and the default webhook URL.
- Confirm the payment through the status endpoint or your webhook, then fulfil the order.
Authentication
Every request carries your secret key in the X-API-Key header. New keys start withpayupi_live_. Keys issued earlier as aupi_live_ still work. Keys are stored hashed, so an old key can never be shown again. Call the API only from your server; never expose the key in browser code.
X-API-Key: payupi_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/jsonCreate order
POST /api/public/v1/create-order
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Order amount in INR, 1 to 10,00,000. |
customer_name | string | No | Shown on the checkout page. |
webhook_url | string | No | HTTPS URL that receives the signed payment.paid webhook. |
link_type | string | No | one_time (default) or reusable. |
curl -X POST https://payupi.co/api/public/v1/create-order \
-H "X-API-Key: payupi_live_your_key" \
-H "Content-Type: application/json" \
-d '{"amount": 1499, "customer_name": "Rahul Sharma", "webhook_url": "https://yoursite.com/payupi-webhook.php"}'{
"ok": true,
"order_id": "ORD20260915A1B2C3",
"slug": "9fkq2wra",
"amount": 1499,
"payable_amount": 1499.37,
"status": "active",
"payment_url": "https://payupi.co/pay/9fkq2wra"
}Always charge payable_amount: it can differ from amount by up to ₹0.99 so each active order stays unique. The customer pays payable_amount; your books are credited with the base amount.
Order status
GET /api/public/v1/order-status?order_id=ORD…
Poll this endpoint from your server (or after the customer returns) to confirm a payment before fulfilment.
curl "https://payupi.co/api/public/v1/order-status?order_id=ORD20260915A1B2C3" \
-H "X-API-Key: payupi_live_your_key"{
"ok": true,
"order": {
"order_id": "ORD20260915A1B2C3",
"slug": "9fkq2wra",
"customer_name": "Rahul Sharma",
"amount": 1499,
"payable_amount": 1499.37,
"status": "paid",
"payer_name": "RAHUL SHARMA",
"paid_at": "2026-09-15T10:21:44.120Z",
"detected_at": "2026-09-15T10:21:52.480Z",
"expires_at": "2026-09-15T10:26:00.000Z"
}
}| Status | Meaning |
|---|---|
active | Waiting for payment. Links expire 5 minutes after creation. |
paid | A matching credit alert was detected. Safe to fulfil. |
expired | No payment detected in time. Create a new order. |
Webhooks
Each API key ships with a webhook secret (whsec_…) shown on the API Keys page. Pass awebhook_url when creating an order and we POST a signed payment.paid event the moment the order is confirmed as paid. You can also save one default webhook URL on the Config page in your dashboard — it is used for every order of your account when the create order call does not send its own webhook_url.
The same Config page holds your success and failed redirect URLs. After the checkout result screen the payer is sent to your URL with ?order_id=…&status=paid or?order_id=…&status=failed. Redirects are per account, so only your own orders use them. Always confirm the final state with the webhook or the order status endpoint before you fulfil. The signature is sent as x-payupi-signature. The previous header x-autoupi-signature is sent as well, with the same value.
POST https://yoursite.com/payupi-webhook.php
X-PayUPI-Signature: t=1789453200,v1=6f3c…9ab
{
"event": "payment.paid",
"order_id": "ORD20260915A1B2C3",
"amount": 1499,
"payable_amount": 1499.37,
"status": "paid",
"payer_name": "RAHUL SHARMA",
"paid_at": "2026-09-15T10:21:44.120Z"
}
signed_payload = "{t}." + raw_request_body
v1 = hex(hmac_sha256(webhook_secret, signed_payload))<?php
// payupi-webhook.php
$secret = "whsec_your_webhook_secret";
$raw = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_PAYUPI_SIGNATURE"] ?? $_SERVER["HTTP_X_AUTOUPI_SIGNATURE"] ?? "";
parse_str(str_replace(",", "&", $header), $parts); // t=..., v1=...
$expected = hash_hmac("sha256", $parts["t"] . "." . $raw, $secret);
if (!hash_equals($expected, $parts["v1"] ?? "")) {
http_response_code(401);
exit("bad signature");
}
$event = json_decode($raw, true);
if ($event["event"] === "payment.paid") {
// idempotent: skip if this order_id is already fulfilled
mark_order_paid($event["order_id"], $event["amount"]);
}
http_response_code(200);
echo "ok";Always treat delivery as at-least-once and keep your handler idempotent.
PHP integration in 3 files
Copy these three files into any PHP site (WordPress, Laravel, CodeIgniter or plain PHP). Only the API key, webhook secret and your database calls need changing.
<?php
class PayUpi {
private string $key;
private string $base;
public function __construct(string $key, string $base = "https://payupi.co/api/public/v1") {
$this->key = $key;
$this->base = $base;
}
private function request(string $url, ?array $body = null): array {
$ch = curl_init($url);
$headers = ["X-API-Key: {$this->key}"];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 20]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
return is_array($res) ? $res : ["ok" => false, "error" => "network_error"];
}
public function createOrder(float $amount, string $customer = "", string $webhook = ""): array {
return $this->request($this->base . "/create-order", [
"amount" => $amount,
"customer_name" => $customer,
"webhook_url" => $webhook,
]);
}
public function status(string $orderId): array {
return $this->request($this->base . "/order-status?order_id=" . urlencode($orderId));
}
public static function verify(string $raw, string $header, string $secret): bool {
parse_str(str_replace(",", "&", $header), $p);
if (empty($p["t"]) || empty($p["v1"])) return false;
return hash_equals(hash_hmac("sha256", $p["t"] . "." . $raw, $secret), $p["v1"]);
}
}<?php
require "payupi.php";
$api = new PayUpi("payupi_live_your_key");
$order = $api->createOrder(1499, "Rahul Sharma", "https://yoursite.com/payupi-webhook.php");
if (empty($order["ok"])) {
die("Could not start payment: " . ($order["error"] ?? "unknown"));
}
// store it so the webhook can find the buyer later
save_order($order["order_id"], $_SESSION["user_id"], $order["amount"]);
header("Location: " . $order["payment_url"]);
exit;<?php
require "payupi.php";
$raw = file_get_contents("php://input");
$sig = $_SERVER["HTTP_X_PAYUPI_SIGNATURE"] ?? $_SERVER["HTTP_X_AUTOUPI_SIGNATURE"] ?? "";
if (!PayUpi::verify($raw, $sig, "whsec_your_webhook_secret")) {
http_response_code(401);
exit("bad signature");
}
$event = json_decode($raw, true);
if (($event["event"] ?? "") === "payment.paid") {
// amount = your original order value; payable_amount = what the customer actually paid
mark_order_paid($event["order_id"], $event["amount"]);
}
http_response_code(200);
echo "ok";No webhook endpoint yet? Poll order-status on your thank-you page instead — the result is the same, just a few seconds slower.
Errors
| HTTP | Code | Fix |
|---|---|---|
| 401 | invalid_api_key | Key missing, revoked or replaced by a regenerate. |
| 400 | invalid_amount | Amount must be greater than 0 and up to 10,00,000. |
| 400 | UPI_NOT_CONFIGURED | Save a UPI ID on Connect Accounts first. |
| 400 | QUOTA_EXCEEDED | QR limit finished — upgrade to Pro. |
| 400 | ALL_PAYMENT_SLOTS_BUSY | Too many pending orders at the same amount; retry shortly. |
| 404 | not_found | The order ID does not belong to this key. |
Pricing
No per-transaction commission — you collect straight into your own UPI account.
Free
₹0to get started- 3 payment QR codes total
- 1 merchant account
- Full REST API access
- Automatic payment confirmation
Pro
₹299per 30 days- 3,000 QR codes per 30 days
- Unlimited payment links
- Signed webhooks
- Priority support
Custom
Talk to ushigh volume- Higher QR limits
- Multiple merchant accounts
- Self-hosted deployment
- Dedicated support
FAQ
No. Money reaches your own UPI account directly; PayUPI only confirms and records the order.
They can't — each active order carries its own unique payable_amount.
5 minutes. After that the order becomes expired and moves to Transactions.
PHP 7.4 or newer with the cURL extension — no composer package required.
Yes. Regenerating issues a new key and instantly disables the previous one.