Aura Pay API

Accept UPI payments with a simple REST API and signed webhooks.

Quick start

Base URL: https://auraissue.lol/public/api/v1

  1. Create an API key in the admin panel (API keys).
  2. From your server, call POST /order/create.
  3. Redirect the customer to the returned checkout_url.
  4. Mark the order paid only after the signed payment.success webhook, or a status call returning PAID.
Never put your API key in browser or app code. Always create orders from your backend.

Authentication

Send your key as a Bearer token on every request (except /health):

Authorization: Bearer gw_live_xxxxxxxxxxxxxxxxxxxxxxxx

gw_live_… keys create real orders. gw_test_… keys create test orders and only work while test mode is on. Live and test data are separate. All requests and responses use JSON.

Create an order

POST/api/v1/order/create

FieldRequiredDescription
merchant_order_idYesYour unique order ID (max 100 chars; letters, numbers, _ - . :).
amountYesAmount in major units, up to 2 decimals (499 or 499.50).
currencyNoMust match the gateway currency (default INR).
customer_id, customer_nameNoInformational.
callback_urlNoWebhook URL for this order. Falls back to the gateway default.
return_urlNoShown as "Return to merchant" after payment.

Returns 201 for a new order. Sending the same merchant_order_id and amount again returns 200 with the same order (safe to retry). A different amount returns 409.

cURL
curl -X POST https://auraissue.lol/public/api/v1/order/create \
  -H "Authorization: Bearer gw_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"merchant_order_id":"ORDER-1001","amount":499,"callback_url":"https://shop.example.com/hook","return_url":"https://shop.example.com/thanks"}'
PHP
<?php
$ch = curl_init('https://auraissue.lol/public/api/v1/order/create');
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GATEWAY_KEY'), 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['merchant_order_id' => 'ORDER-1001', 'amount' => 499]),
]);
$order = json_decode(curl_exec($ch), true);
if (!empty($order['success'])) { header('Location: ' . $order['checkout_url']); exit; }
Python
import os, requests
r = requests.post("https://auraissue.lol/public/api/v1/order/create", timeout=20,
    headers={"Authorization": "Bearer " + os.environ["GATEWAY_KEY"]},
    json={"merchant_order_id": "ORDER-1001", "amount": 499})
order = r.json()
print(order["checkout_url"])
Node.js
const r = await fetch('https://auraissue.lol/public/api/v1/order/create', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.GATEWAY_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ merchant_order_id: 'ORDER-1001', amount: 499 }),
});
const order = await r.json();
console.log(order.checkout_url);

Response

{
  "success": true,
  "order_id": "GW_AB12CD34EF56GH78",
  "merchant_order_id": "ORDER-1001",
  "reference": "K7M2QX9P",
  "amount": 499,
  "currency": "INR",
  "status": "PENDING",
  "upi_id": "shop@bank",
  "upi_uri": "upi://pay?pa=shop@bank&pn=Shop&tn=K7M2QX9P&am=499.00&cu=INR",
  "qr_url": "https://auraissue.lol/public/qr/GW_AB12CD34EF56GH78.svg",
  "checkout_url": "https://auraissue.lol/public/checkout/GW_AB12CD34EF56GH78",
  "expires_at": "2026-10-02T10:15:00+05:30",
  "livemode": true
}

Get order status

GET/api/v1/order/status/{order_id}

Add ?refresh=1 to make the gateway check the payment provider right now (rate-limited).

curl https://auraissue.lol/public/api/v1/order/status/GW_AB12CD34EF56GH78 \
  -H "Authorization: Bearer gw_live_xxx"
{
  "success": true,
  "order_id": "GW_AB12CD34EF56GH78",
  "merchant_order_id": "ORDER-1001",
  "amount": 499, "currency": "INR",
  "status": "PAID",
  "reference": "K7M2QX9P",
  "transaction_id": "…",
  "paid_at": "2026-10-02 10:05:11",
  "expires_at": "2026-10-02T10:15:00+05:30",
  "failure_reason": null,
  "webhook": { "event": "payment.success", "status": "DELIVERED", "attempts": 1 },
  "livemode": true
}

GET/api/v1/health — no authentication; returns {"success":true,"status":"ok"}.

Webhooks

When an order changes state the gateway sends a POST with a JSON body to your callback_url. Events: payment.success, payment.failed, payment.expired.

HeaderMeaning
X-Gateway-TimestampUnix time (seconds) when the request was signed.
X-Gateway-SignatureHex HMAC-SHA256 of timestamp + "." + raw body using your webhook secret.
X-Gateway-EventEvent name.
X-Gateway-Event-IdSame value on every retry. Use it to ignore duplicates.
X-Gateway-AttemptDelivery attempt number.
{
  "event": "payment.success",
  "order_id": "GW_AB12CD34EF56GH78",
  "merchant_order_id": "ORDER-1001",
  "amount": 499, "currency": "INR",
  "reference": "K7M2QX9P",
  "transaction_id": "…",
  "status": "PAID",
  "livemode": true,
  "timestamp": "2026-10-02T10:05:11+05:30"
}

Verify the signature

Always use the raw request body (not re-encoded JSON), compare in constant time, and reject timestamps older than 5 minutes. Reply with any 2xx status; anything else is retried.

PHP
<?php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_GATEWAY_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, getenv('WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig) || abs(time() - (int)$ts) > 300) { http_response_code(400); exit; }
$event = json_decode($raw, true);
// de-duplicate on $_SERVER['HTTP_X_GATEWAY_EVENT_ID'], check amount + merchant_order_id, then fulfil
http_response_code(200); echo 'ok';
Python (Flask)
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"].encode()

@app.post("/hook")
def hook():
    raw = request.get_data()
    ts = request.headers.get("X-Gateway-Timestamp", "")
    sig = request.headers.get("X-Gateway-Signature", "")
    expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig) or abs(time.time() - int(ts or 0)) > 300:
        abort(400)
    event = request.get_json()
    return "ok", 200
Node.js (Express)
const crypto = require('crypto');
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.header('X-Gateway-Timestamp') || '';
  const sig = req.header('X-Gateway-Signature') || '';
  const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(ts + '.' + req.body).digest('hex');
  const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  res.sendStatus(200);
});

Retries

Failed deliveries are retried automatically (default: after 1 min, 5 min, 15 min, 1 h, 6 h; up to 6 attempts). Callback URLs must be public http(s) addresses.

Order statuses

CREATED / PENDINGWaiting for payment.
PAIDPayment verified with the exact amount.
FAILEDPayment could not be completed.
EXPIREDNot paid before the expiry time.
CANCELLEDCancelled.
AMOUNT_MISMATCHA payment was found with a different amount. It is never marked paid automatically; an admin reviews it.

Errors

{ "success": false, "error": { "code": "invalid_amount", "message": "amount must be a positive number with at most 2 decimals." } }
HTTPCodes
400invalid_json
401missing_api_key invalid_api_key test_mode_disabled
403merchant_disabled test_key_required not_a_test_order
404order_not_found not_found
405method_not_allowed
409duplicate_merchant_order_id order_not_pending order_already_paid
413payload_too_large
422invalid_amount amount_out_of_range invalid_merchant_order_id invalid_callback_url invalid_return_url invalid_currency
429rate_limited (honour the Retry-After header)
500server_error gateway_misconfigured

Testing

Use a gw_test_ key (available when the gateway is in test mode). Test orders never touch the payment provider and can be driven with the simulator:

POST/api/v1/test/simulate/{order_id}

curl -X POST https://auraissue.lol/public/api/v1/test/simulate/GW_AB12CD34EF56GH78 \
  -H "Authorization: Bearer gw_test_xxx" -H "Content-Type: application/json" \
  -d '{"scenario":"PAID"}'

Scenarios: PENDING, PAID, FAILED, WRONG_AMOUNT, EXPIRED. Outside test mode this endpoint returns 404.

Go-live checklist: switch to your gw_live_ key, test a small real payment, verify webhook signatures, and store your webhook secret server-side only.

Aura Pay · Support