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.

BASE URLhttps://payupi.co/api/public/v1
FORMATapplication/json
AUTH HEADERX-API-Key

Quickstart

  1. Create an account and sign in to the merchant console.
  2. Connect an account — pick PhonePe or Paytm and complete the setup on the Connect Accounts page.
  3. Generate an API key on the API Keys page. Copy it once; regenerating replaces the old key.
  4. Create an order from your server and redirect the customer to the returned payment_url.
  5. Set your URLs on the Config page — success redirect, failed redirect and the default webhook URL.
  6. 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.

header
X-API-Key: payupi_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Create order

POST /api/public/v1/create-order

FieldTypeRequiredDescription
amountnumberYesOrder amount in INR, 1 to 10,00,000.
customer_namestringNoShown on the checkout page.
webhook_urlstringNoHTTPS URL that receives the signed payment.paid webhook.
link_typestringNoone_time (default) or reusable.
curl
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"}'
200 response
{
  "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
curl "https://payupi.co/api/public/v1/order-status?order_id=ORD20260915A1B2C3" \
  -H "X-API-Key: payupi_live_your_key"
200 response
{
  "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"
  }
}
StatusMeaning
activeWaiting for payment. Links expire 5 minutes after creation.
paidA matching credit alert was detected. Safe to fulfil.
expiredNo 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.

payload
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
<?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.

1. payupi.php — tiny helper class
<?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"]);
  }
}
2. checkout.php — start a payment
<?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;
3. payupi-webhook.php — get paid automatically
<?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

HTTPCodeFix
401invalid_api_keyKey missing, revoked or replaced by a regenerate.
400invalid_amountAmount must be greater than 0 and up to 10,00,000.
400UPI_NOT_CONFIGUREDSave a UPI ID on Connect Accounts first.
400QUOTA_EXCEEDEDQR limit finished — upgrade to Pro.
400ALL_PAYMENT_SLOTS_BUSYToo many pending orders at the same amount; retry shortly.
404not_foundThe 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
Create account

Custom

Talk to ushigh volume
  • Higher QR limits
  • Multiple merchant accounts
  • Self-hosted deployment
  • Dedicated support
Create account

FAQ

Do I need a payment gateway account?

No. Money reaches your own UPI account directly; PayUPI only confirms and records the order.

What if two customers pay the same amount?

They can't — each active order carries its own unique payable_amount.

How long is a link valid?

5 minutes. After that the order becomes expired and moves to Transactions.

Which PHP version do I need?

PHP 7.4 or newer with the cURL extension — no composer package required.

Can I regenerate my API key?

Yes. Regenerating issues a new key and instantly disables the previous one.