Create an order
Request and response fields for creating an order, how replays work, and every error this endpoint returns.
POST /api/v1/orders creates a payment link, or replays the existing one if you send the same merchant_order_id again.
Request fields
| Field | Type | Rules |
|---|---|---|
amount |
number | Required. More than ₹0 and at most ₹10,00,000. The amount is rounded to the nearest paisa (amount * 100, rounded), not rejected for extra decimals. |
merchant_order_id |
string | Required. Your own order id, trimmed, 1 to 120 characters. Used for idempotency: send the same value again to get the same order back. |
success_url |
string | Required. An absolute https:// or http:// URL, at most 500 characters, with no spaces and no username or password in it. The customer returns here after a successful payment. |
failure_url |
string | Required. An absolute https:// or http:// URL, at most 500 characters, with no spaces and no username or password in it. The customer returns here after an expired or failed payment. |
webhook_url |
string | Required. A public https:// or http:// URL on your own server, at most 500 characters. It must not be localhost, a single-label host, a .localhost, .internal, .local or .home.arpa name, or a private or reserved IP address. It is checked again every time UPINOW sends to it. |
customer |
object | Optional. { email, name } for your reference: email must look like an email address, name at most 120 characters. Other fields are ignored. |
payment_account_id |
string | Optional. Which of your UPI accounts this order is created on. Leave it out to use your default account. Copy the id from Settings, UPI accounts, "Account ID (for the API)" on the account you want. |
A domain field is accepted for backward compatibility but has no effect; leave it out.
Response (201 Created)
{
"order_id": "UPN-0123456789",
"payable_amount": 499.07,
"status": "pending",
"expires_at": "2026-09-08T17:30:00.000Z",
"payment_url": "https://app.upinow.in/pay/UPN-0123456789",
"upi_uri": "upi://pay?pa=merchant@upi&pn=Sample%20Store&am=499.07&cu=INR&tn=UPN-0123456789",
"qr_base64": "data:image/svg+xml;base64,...",
"payment_account_id": "b3f8a1c4-2e6d-4a3b-9c0f-1234567890ab"
}| Field | Meaning |
|---|---|
order_id |
The UPINOW order id, always UPN- followed by 10 digits (older orders can start with PANME-). |
payable_amount |
The exact amount the customer must pay; see below. |
status |
Always pending on a fresh order. |
expires_at |
When the link stops being payable: 5 minutes after creation. |
payment_url |
A hosted page the customer can open directly; see Pay page and redirects. |
upi_uri |
A upi://pay deep link you can turn into a button or a QR code yourself. |
qr_base64 |
The same link as a ready-made QR code, an SVG data: image. Can be null if the QR could not be generated; fall back to upi_uri. |
payment_account_id |
The UPI account this order was created on: the one you asked for, or your default account when you left the field out. |
Payable amount
payable_amount is your amount plus a small offset, usually ₹0.01 to ₹0.99, so that this order's exact amount is unique and UPINOW can match the bank alert to it without any other reference. When many links share the same base amount at once, the offset can go up to ₹2.99. Always show the customer payable_amount, and ask them to pay it exactly, paise included.
Replaying an order
Calling create again with a merchant_order_id you already used returns that same order with 200 OK (not 201) and idempotent_replay: true added to the response. The new body you sent is ignored, including payment_account_id: a replay always answers with the account the order was actually created on, never a different one you might pass on the retry. status reflects the order's current state, not the state it was in when created: it can be paid or expired by the time you replay it. This makes retrying a create call after a network error safe.
Reusing a merchant_order_id whose link has already expired replays that same expired order; it does not start a new payment. Give each new payment attempt a fresh merchant_order_id.
Errors
This endpoint can answer with any of the errors on the Errors page. The ones you are most likely to see while integrating:
| Status | Code | When |
|---|---|---|
| 400 | invalid_request |
The body is not JSON, a field fails validation, or the webhook_url is refused. |
| 400 | invalid_payment_account |
payment_account_id is malformed, unknown, removed, or belongs to a different business. One answer for every case, so nothing leaks about which ids exist. |
| 401 | missing_api_key / invalid_api_key |
The key is missing, unknown, or revoked. |
| 402 | payment_account_paused |
The account you chose (or your default account) is paused because your plan's UPI account limit was reached; see upgrade_url in the response. |
| 403 | merchant_disabled |
The business is disabled, including a business inside its 7-day account-deletion window. |
| 409 | upi_not_configured |
You have not added a UPI ID yet. |
| 402 | plan_limit_reached |
This month's collections reached your plan's limit. |
| 503 | all_payment_slots_busy |
Too many open orders share this amount right now; retry shortly. |
A malformed request body, for example, comes back as 400 invalid_request.
Samples
curl -X POST https://app.upinow.in/api/v1/orders \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 499,
"merchant_order_id": "order_1024",
"success_url": "https://example.com/thanks",
"failure_url": "https://example.com/cart",
"webhook_url": "https://example.com/upinow-webhook.php"
}'<?php
$apiKey = 'YOUR_API_KEY';
$body = json_encode([
'amount' => 499,
'merchant_order_id' => 'order_' . time(),
'success_url' => 'https://example.com/thanks',
'failure_url' => 'https://example.com/cart',
'webhook_url' => 'https://example.com/upinow-webhook.php',
]);
$context = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\nX-API-Key: $apiKey\r\n",
'content' => $body,
'ignore_errors' => true,
]]);
$response = file_get_contents('https://app.upinow.in/api/v1/orders', false, $context);
$order = json_decode($response, true);
if (!isset($order['order_id'])) {
exit('Error: ' . $response . PHP_EOL);
}
echo 'Order: ' . $order['order_id'] . PHP_EOL;
echo 'Pay link: ' . $order['payment_url'] . PHP_EOL;const res = await fetch("https://app.upinow.in/api/v1/orders", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": "YOUR_API_KEY" },
body: JSON.stringify({
amount: 499,
merchant_order_id: "order_" + Date.now(),
success_url: "https://example.com/thanks",
failure_url: "https://example.com/cart",
webhook_url: "https://example.com/upinow-webhook",
}),
});
const order = await res.json();
if (!res.ok) throw new Error(JSON.stringify(order));
console.log("Order:", order.order_id);
console.log("Pay link:", order.payment_url);import json, time, urllib.error, urllib.request
body = json.dumps({
"amount": 499,
"merchant_order_id": f"order_{int(time.time())}",
"success_url": "https://example.com/thanks",
"failure_url": "https://example.com/cart",
"webhook_url": "https://example.com/upinow-webhook",
}).encode()
req = urllib.request.Request(
"https://app.upinow.in/api/v1/orders", data=body, method="POST",
headers={"Content-Type": "application/json", "X-API-Key": "YOUR_API_KEY"},
)
try:
with urllib.request.urlopen(req) as res:
order = json.load(res)
except urllib.error.HTTPError as e:
raise SystemExit(e.read().decode())
print("Order:", order["order_id"])
print("Pay link:", order["payment_url"])