TaknaPay.indeveloper docsRegister your siteStatus

UPI payment gateway API

Accept UPI payments with a dynamic QR code and a hosted checkout page. Payments are verified automatically from the bank confirmation email, so you never touch card or bank details. Your server creates an order, sends the customer to the checkout, and gets told when it is paid.

Overview

1. Create the orderYour server calls POST /api/create-payment with the amount and your own order ID.
2. Customer paysSend them to the returned checkoutUrl. They scan the QR or tap Pay with UPI app.
3. We verifyThe gateway matches the payment confirmation email to the order, usually within seconds.
4. You get toldA signed webhook reaches your server and the customer returns to your site.
Never trust the browser redirect alone. The status=SUCCESS in the return URL can be edited by anyone. Always confirm with the webhook or with GET /api/check-status/:orderId before delivering anything.

Quick start

You need an API key. If you run this gateway yourself, it is the API_SECRET_KEY you set on the server. If you are using someone else's gateway, register here and use the key shown in your dashboard after approval.

curl -X POST https://taknapay.in/api/create-payment \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount": 99, "orderId": "ORDER_1001"}'
const r = await fetch('https://taknapay.in/api/create-payment', {
  method: 'POST',
  headers: { 'x-api-key': process.env.GATEWAY_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ amount: 99, orderId: 'ORDER_1001' })
});
const data = await r.json();
// send the customer to:
const url = data.checkoutUrl + '&redirectUrl=' + encodeURIComponent('https://yoursite.com/thanks');
<?php
$ch = curl_init('https://taknapay.in/api/create-payment');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('GATEWAY_API_KEY'), 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode(['amount' => 99, 'orderId' => 'ORDER_1001']),
]);
$data = json_decode(curl_exec($ch), true);
header('Location: ' . $data['checkoutUrl'] . '&redirectUrl=' . urlencode('https://yoursite.com/thanks'));
import os, requests, urllib.parse
r = requests.post('https://taknapay.in/api/create-payment',
    headers={'x-api-key': os.environ['GATEWAY_API_KEY']},
    json={'amount': 99, 'orderId': 'ORDER_1001'}, timeout=15)
data = r.json()
url = data['checkoutUrl'] + '&redirectUrl=' + urllib.parse.quote('https://yoursite.com/thanks', safe='')

Authentication

Server-to-server calls send your key in the x-api-key header. Keep it on your server only, never in browser code or a mobile app.

x-api-key: YOUR_API_KEY

The same key signs the webhooks we send you, so you can prove a webhook really came from the gateway.

Create payment

POST/api/create-paymentneeds x-api-key
FieldTypeRules
amountnumberRupees. Greater than 0, at most 10,00,000.
orderIdstringYour unique ID. 1 to 100 characters: letters, digits and _ - . :
{
  "success": true,
  "orderId": "ORDER_1001",
  "amount": 99,
  "status": "PENDING",
  "qrCodeUrl": "data:image/png;base64,...",
  "checkoutUrl": "https://taknapay.in/index.html?orderId=ORDER_1001&amount=99"
}

Checkout and redirect

Send the customer to checkoutUrl and append &redirectUrl= with your return page, URL-encoded. After the payment they land on our success page, then come back to your redirectUrl with two values added:

OutcomeCustomer seesYour return URL gets
Paid/success.html?orderId=...&status=SUCCESS
Cancelled/cancel.html?orderId=...&status=CANCELLED

Check status

GET/api/check-status/:orderId
{ "success": true, "status": "SUCCESS", "amount": 99, "orderId": "ORDER_1001" }

If the order is still pending, this call also triggers a fresh look at the mailbox, so it is the quickest way to confirm a payment. Do not call it more than once every few seconds. Limit: 40 requests per minute per IP.

GET/api/order/:orderId

Public read-only details used by the checkout page: amount, status, QR code and UPI link.

Webhooks

When a payment is verified, the gateway sends a POST to your webhook URL. For registered merchants, set it in your dashboard (https only). On a self-hosted gateway it is SITE_WEBHOOK_URL.

POST https://yoursite.com/api/payment-webhook
Content-Type: application/json
x-timestamp: 1767225600000
x-signature: 9f2c...  (hex)

{ "event": "payment.success", "orderId": "ORDER_1001", "status": "SUCCESS", "amount": 99, "paidAt": "2026-01-01T10:00:00.000Z" }

Verify the signature. It is an HMAC-SHA256 of timestamp + "." + raw body, using your API key, written as hex. Use the raw request body, not re-encoded JSON, and reject old timestamps.

const crypto = require('crypto');
app.post('/api/payment-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.headers['x-timestamp'] || '';
  const sig = req.headers['x-signature'] || '';
  const body = req.body.toString();
  const expected = crypto.createHmac('sha256', process.env.GATEWAY_API_KEY).update(ts + '.' + body).digest('hex');
  const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok || Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return res.sendStatus(401);
  const e = JSON.parse(body);
  if (e.status === 'SUCCESS') { /* mark the order paid. Do it only once per orderId. */ }
  res.sendStatus(200);
});
<?php
$body = file_get_contents('php://input');
$ts   = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$sig  = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $body, getenv('GATEWAY_API_KEY'));
if (!hash_equals($expected, $sig) || abs(microtime(true) * 1000 - (float)$ts) > 300000) { http_response_code(401); exit; }
$e = json_decode($body, true);
if ($e['status'] === 'SUCCESS') { /* mark the order paid, once */ }
http_response_code(200);
import hmac, hashlib, time, os
from flask import request, abort

@app.post('/api/payment-webhook')
def webhook():
    body = request.get_data()
    ts = request.headers.get('x-timestamp', '')
    expected = hmac.new(os.environ['GATEWAY_API_KEY'].encode(), ts.encode() + b'.' + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get('x-signature', '')) or abs(time.time() * 1000 - float(ts or 0)) > 300000:
        abort(401)
    e = request.get_json()
    if e['status'] == 'SUCCESS':
        pass  # mark the order paid, once
    return '', 200

Order statuses

StatusMeaning
PENDINGOrder created, payment not seen yet. Checked for PENDING_WINDOW_HOURS (24 by default).
SUCCESSPayment found and the amount matched exactly. Final.
FAILEDMarked failed by the admin.

The amount must match exactly. Paying 150 for a 15 order is not accepted as payment of that order.

Errors and limits

CodeWhen
400Missing or invalid amount or orderId.
401Missing or wrong API key.
403Merchant account is not approved, or is suspended.
404Order not found.
409orderId already used by someone else.
429Too many requests. Wait a minute and retry.
500Server problem. Retry shortly.

All errors look like { "success": false, "message": "..." }. Limits per IP: order lookup 60 per minute, status check 40 per minute, merchant sign-up 5 per hour, merchant login 10 per 15 minutes. Requests larger than 50 KB are rejected.

Merchant accounts

Anyone can ask to use the gateway on their own site. This is how it works:

  1. Open /merchant.html and register with your PAN number, a photo of your PAN card (KYC is required), your business name, email, website and an optional https webhook URL. You receive a private login token, shown once.
  2. The admin checks your KYC in the admin panel under Merchants and approves or rejects your request. An account cannot be approved without PAN details.
  3. Log in to the dashboard with your email and token. Once approved, your API key appears there, along with your orders, revenue and a webhook tester.
  4. Use the key with the API above. Orders you create are yours: only you receive their webhooks.
You can regenerate your key from the dashboard at any time. The old key stops working immediately. If you lose your login token, ask the admin for a new one.

Payments go to the gateway owner's UPI account, as set by the admin. Settlement to merchants is arranged directly with the admin.

Plans and fees

Every approved merchant starts on the Free plan. Fees are taken from each successful order and shown per order in the dashboard.

Free (default)Pro
Monthly fee₹0₹499 (30 days)
Fee per transaction5% of the amount₹1
Settlement time3 working days3 working days
Payout charge: bank (NEFT)1%Free
Payout charge: bank (IMPS)1.5%0.5%
Payout charge: UPI2%Free
Refund charge1% of the refund1% of the refund

Buy Pro from the dashboard (₹499 is deducted from your available balance) or ask the admin to activate it. When Pro ends, the account returns to Free. A plan change applies to new orders only: each order keeps the fee of the plan it was created under.

Payouts

  1. Money from a successful order is available to withdraw 3 working days (Monday to Friday) after payment, minus the plan fee.
  2. In the dashboard open Request a payout. Pick Bank account (NEFT), Bank account (IMPS) or UPI. For a bank account enter the holder name, account number and IFSC code. For UPI enter your UPI ID. Minimum amount is ₹100.
  3. The payout charge (see the table above) is cut from the amount. You receive amount minus charge.
  4. The admin receives your request, sends the money and enters the UTR number, then accepts it. The request turns PAID and shows the UTR in your payout history.
  5. If the admin rejects a request, the full amount returns to your available balance.
Check your account details before submitting. A transfer to wrong details cannot be reversed.

Refunds

  1. In the dashboard use the Refund button next to a paid order, or open Refunds and enter the order ID. Full or partial refunds are possible. Add a reason, and optionally the customer UPI ID.
  2. The admin refunds the customer from the same payment account and enters the refund UTR. The request turns REFUNDED.
  3. The payment fee of the order (5% on Free, ₹1 on Pro) is never refunded. It stays cut.
  4. Every refund has a 1% refund charge on the refunded amount, on all plans. It is cut from your available balance.
  5. From your balance we take refund amount plus refund charge. A pending refund already lowers it; a rejected refund restores everything. Your available balance must cover the charge, and your total balance (including money still settling) must cover the refund.

Built-in pages

PagePurpose
/index.htmlHosted checkout: QR, UPI app button, check status, cancel
/success.htmlPayment successful. Verified on the server, then returns the customer to you
/cancel.htmlPayment cancelled or failed, with try again and return options
/merchant.htmlSign up and merchant dashboard
/admin.htmlAdmin panel: transactions, merchants, health, CSV export
/ping.htmlLive status and keep-awake page
/healthz and /api/pingLight endpoints for uptime monitors

Feature list

Automatic verificationMatches the bank confirmation email to the order. No manual work.
Exact amount matching15 never matches 150, 115 or 1,500.
Signed, retried webhooksHMAC signature, 3 quick tries, then every minute for 48 hours.
Merchant sign-upOther sites register, wait for approval, then get their own key.
Admin panelRevenue chart, search, filters, per-merchant view, mark paid or failed, resend webhook, CSV export, light and dark theme.
Safe by defaultRate limits, input checks, timing-safe key checks, locked redirects, webhook URLs limited to public https addresses.
FastCached QR codes, database index, narrow mailbox search, cached static files.
Never sleepsSelf-ping every few minutes, plus a GitHub Actions workflow and a status page.

Self-hosting

Deploy on Render with GitHub. Set these environment variables on the service:

VariableWhat it does
MONGO_URIrequiredMongoDB connection string
API_SECRET_KEYrequiredKey for your own main site. Also signs the merchant login sessions
GMAIL_USER, GMAIL_APP_PASSWORDrequiredMailbox that receives the payment emails (IMAP enabled, app password)
ADMIN_SECRET_PASSfor admin panelAdmin panel password
BUSINESS_UPI, PAYEE_NAMEoptionalUPI address and name shown in the payment app
SITE_WEBHOOK_URLoptionalWebhook for orders from your own main site
PENDING_WINDOW_HOURSoptionalHow long an unpaid order keeps being checked (default 24)
KEEP_ALIVE, KEEP_ALIVE_URL, PING_INTERVAL_MINoptionalSelf-ping on or off, its target, and its interval in minutes (default 5)

Keeping a free Render service awake: the server pings itself, but a sleeping server cannot wake itself. Add the GitHub workflow in .github/workflows/keep-alive.yml (set the repository variable PING_URL to https://taknapay.in/healthz), or add the same URL to UptimeRobot or cron-job.org every 5 minutes. /ping.html shows whether it is working.

Registered merchants' API keys are stored in your database so they can see them in their dashboard. Protect your MongoDB credentials accordingly.

What is new

Built by Sibaditya Pal · TaknaPay.in