Plain PHP guide
A complete plain PHP integration you can copy, run and adapt, with no framework and no database required.
On this page
This guide is a small, complete integration in plain PHP: no framework, no Composer packages, and no database (it stores orders in a JSON file so you can run it right away; swap that for your own database when you go live).
Requirements
- PHP 7.4 or newer.
allow_url_fopenturned on (the default on most hosts; these files usefile_get_contentsto call the API).- The
opensslextension (bundled with PHP; needed sofile_get_contentscan call anhttps://URL).hash_hmac, used to verify webhooks, needs no extension: it is part of PHP's always-onhashextension. - A public HTTPS address for
upinow-webhook.php, since UPINOW has to reach it from the internet. For local testing, run a tunnel service (for example ngrok or Cloudflare Tunnel) and use the address it gives you as your webhook URL while you test.
Folder layout
your-site/
upinow-data/ created by store.php, outside the public folder
public_html/
config.php
store.php
checkout.php
upinow-webhook.php
thanks.php
create-order.php only for the embedded widgetKeep upinow-data/ outside public_html/ so nobody can download orders.json directly from a browser.
config.php
Your API key, webhook secret and site address live here. Fill in your own values from Integrations before you go live.
<?php
// Your UPINOW settings. Keep real keys out of version control.
const UPINOW_BASE = 'https://app.upinow.in';
const UPINOW_API_KEY = 'YOUR_API_KEY';
const UPINOW_WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET';
const MY_SITE = 'https://example.com';
// Calls the UPINOW API and returns the decoded JSON (an 'error' key on failure).
function upinow_api(string $method, string $path, ?array $body = null): array
{
$header = "X-API-Key: " . UPINOW_API_KEY . "\r\nAccept: application/json\r\n";
$http = ['method' => $method, 'ignore_errors' => true, 'timeout' => 15];
if ($body !== null) {
$header .= "Content-Type: application/json\r\n";
$http['content'] = json_encode($body);
}
$http['header'] = $header;
$raw = @file_get_contents(UPINOW_BASE . $path, false, stream_context_create(['http' => $http]));
$data = $raw === false ? null : json_decode($raw, true);
return is_array($data) ? $data : ['error' => 'network_error'];
}store.php
A tiny order store so this guide runs without a database. It keeps one JSON file, with a lock around every read and write so two requests can never corrupt it.
<?php
// A tiny order store in a JSON file, so this guide runs without a database.
// It lives outside the public folder. Use your real database in production.
const STORE_FILE = __DIR__ . '/../upinow-data/orders.json';
// Runs $change on the orders array while holding a lock, then saves it.
function store_update(callable $change)
{
$dir = dirname(STORE_FILE);
if (!is_dir($dir)) {
mkdir($dir, 0700, true);
}
$fp = fopen(STORE_FILE, 'c+');
flock($fp, LOCK_EX);
$text = stream_get_contents($fp);
$orders = $text ? (json_decode($text, true) ?: []) : [];
$result = $change($orders);
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($orders, JSON_PRETTY_PRINT));
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return $result;
}checkout.php
Open this page to start a payment: it creates the order and sends the customer straight to the UPINOW pay page.
<?php
require __DIR__ . '/config.php';
require __DIR__ . '/store.php';
$amount = 499; // Your cart total in rupees, worked out on your server.
$myOrderId = 'order_' . time() . '_' . bin2hex(random_bytes(3));
$order = upinow_api('POST', '/api/v1/orders', [
'amount' => $amount,
'merchant_order_id' => $myOrderId,
'success_url' => MY_SITE . '/thanks.php',
'failure_url' => MY_SITE . '/thanks.php',
'webhook_url' => MY_SITE . '/upinow-webhook.php',
]);
if (!isset($order['order_id'], $order['payment_url'])) {
http_response_code(502);
exit('Could not start the payment: ' . htmlspecialchars($order['error'] ?? 'unknown_error'));
}
store_update(function (array &$orders) use ($myOrderId, $amount, $order) {
$orders[$myOrderId] = [
'amount' => $amount,
'upinow_order_id' => $order['order_id'],
'status' => 'pending',
'paid_at' => null,
];
});
header('Location: ' . $order['payment_url']);
exit;upinow-webhook.php
UPINOW posts to this URL when your order is paid, expired or failed. It checks the signature first, then marks the order paid in the store, only once.
<?php
require __DIR__ . '/config.php';
require __DIR__ . '/store.php';
$raw = file_get_contents('php://input');
$parts = [];
foreach (explode(',', $_SERVER['HTTP_X_SIGNATURE'] ?? '') as $pair) {
[$k, $v] = array_pad(explode('=', $pair, 2), 2, '');
$parts[trim($k)] = trim($v);
}
$t = $parts['t'] ?? '';
$v1 = $parts['v1'] ?? '';
$expected = hash_hmac('sha256', $t . '.' . $raw, UPINOW_WEBHOOK_SECRET);
if ($t === '' || $v1 === '' || !hash_equals($expected, $v1)) {
http_response_code(401);
exit('invalid signature');
}
if (abs(time() - (int) $t) > 300) {
http_response_code(400);
exit('stale');
}
$event = json_decode($raw, true);
$myOrderId = $event['merchant_order_id'] ?? '';
if (($event['type'] ?? '') === 'payment.paid' && $myOrderId !== '') {
store_update(function (array &$orders) use ($myOrderId, $event) {
$order = $orders[$myOrderId] ?? null;
if ($order === null || $order['status'] === 'paid') {
return; // Unknown order, or already paid: nothing to do.
}
if ((float) $event['amount'] !== (float) $order['amount']) {
error_log('UPINOW amount mismatch for ' . $myOrderId);
return;
}
$orders[$myOrderId]['status'] = 'paid';
$orders[$myOrderId]['paid_at'] = $event['paid_at'];
// Fulfil the order here: ship it, credit the wallet, email the customer.
});
}
// payment.expired and payment.failed leave the order unpaid. A late payment.paid can still follow.
echo 'ok';thanks.php
Both success_url and failure_url point here. It never trusts the URL by itself; it asks the API for the real status.
<?php
require __DIR__ . '/config.php';
// UPINOW adds ?order_id=UPN-...&status=... to your return URL. Never trust status: ask the API.
$upinowId = $_GET['order_id'] ?? '';
if (!preg_match('/^(UPN|PANME)-\d{10}$/', $upinowId)) {
http_response_code(400);
exit('Missing order.');
}
$order = upinow_api('GET', '/api/v1/orders/' . rawurlencode($upinowId));
$status = $order['status'] ?? 'unknown';
if ($status === 'paid') {
echo 'Thank you! Payment received for order ' . htmlspecialchars($order['merchant_order_id'] ?? '');
} elseif ($status === 'pending') {
echo 'We are confirming your payment. Refresh this page in a few seconds.';
} else {
echo 'This payment did not complete. You can try again.';
}Using the embedded widget
If you would rather keep the customer on your own page instead of redirecting to the pay page, mount the embedded QR widget and point it at create-order.php so your API key stays on your server:
<?php
// The embedded widget posts {"amount": ...} here. Your API key stays on the server.
// For a cart, work out the amount on your server instead of trusting the browser.
require __DIR__ . '/config.php';
require __DIR__ . '/store.php';
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$amount = (float) ($input['amount'] ?? 0);
if ($amount <= 0) {
http_response_code(400);
exit(json_encode(['error' => 'invalid_request']));
}
$myOrderId = 'order_' . time() . '_' . bin2hex(random_bytes(3));
$order = upinow_api('POST', '/api/v1/orders', [
'amount' => $amount,
'merchant_order_id' => $myOrderId,
'success_url' => MY_SITE . '/thanks.php',
'failure_url' => MY_SITE . '/thanks.php',
'webhook_url' => MY_SITE . '/upinow-webhook.php',
]);
if (!isset($order['order_id'])) {
http_response_code(502);
} else {
// Record it the same way checkout.php does, so upinow-webhook.php can find it and mark it
// paid. Without this, the widget's own webhooks are always an "Unknown order" and are dropped.
store_update(function (array &$orders) use ($myOrderId, $amount, $order) {
$orders[$myOrderId] = [
'amount' => $amount,
'upinow_order_id' => $order['order_id'],
'status' => 'pending',
'paid_at' => null,
];
});
}
echo json_encode($order);Try it
- Put your real API key and webhook secret in
config.php, and your own public site address inMY_SITE. - Open
checkout.phpin a browser. It redirects you to the UPINOW pay page. - Pay the exact amount shown, paise included, with any UPI app.
- Watch
upinow-data/orders.json: itsstatusflips frompendingtopaidonce UPINOW's webhook reachesupinow-webhook.php. - You land back on
thanks.php, which now says "Thank you! Payment received for order ...".
Going live
Move UPINOW_API_KEY and UPINOW_WEBHOOK_SECRET out of config.php and into environment variables, or a file kept outside your web root, so they can never be downloaded by mistake. Replace store.php with your real database, keeping the same rule it already follows: check whether the order is already paid before you change anything, so a retried or duplicate webhook can never credit an order twice.