Skip to content
upınow

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_fopen turned on (the default on most hosts; these files use file_get_contents to call the API).
  • The openssl extension (bundled with PHP; needed so file_get_contents can call an https:// URL). hash_hmac, used to verify webhooks, needs no extension: it is part of PHP's always-on hash extension.
  • 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

text
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 widget

Keep 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.

config.php
<?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.

store.php
<?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.

checkout.php
<?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.

upinow-webhook.php
<?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.

thanks.php
<?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:

create-order.php
<?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

  1. Put your real API key and webhook secret in config.php, and your own public site address in MY_SITE.
  2. Open checkout.php in a browser. It redirects you to the UPINOW pay page.
  3. Pay the exact amount shown, paise included, with any UPI app.
  4. Watch upinow-data/orders.json: its status flips from pending to paid once UPINOW's webhook reaches upinow-webhook.php.
  5. 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.