Overview
The Uplinq Payments API is a small, opinionated REST API for taking payments through any connected provider. The integration loop is:
- Create a payment session from your backend.
- Redirect the customer to the returned hosted checkout URL.
- Receive a signed webhook when the payment finalises.
All requests use JSON. All amounts are integers in the smallest unit of the currency (e.g. paise for INR, cents for USD). Timestamps are ISO-8601 UTC.
Base URL & versioning
https://api.uplinqgateway.com/v1Every endpoint is mounted under this base. Paths in this reference are written without the base (e.g. /v1-sessions means https://api.uplinqgateway.com/v1/v1-sessions). Breaking changes ship as a new path prefix; additive changes are made in place.
Authentication
Authenticate every server-to-server request with an API key issued from the merchant portal. Pass it as a Bearer token in the Authorization header, formatted as <key_id>:<secret>.
Authorization: Bearer kid_live_abc123:sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxNever embed the secret in browser code or mobile apps. The hosted checkout URL is what you give the customer — it does not require an API key.
IP allowlist (optional)
Each merchant can pin server-to-server API calls to a set of source IPs (IPv4, IPv6, or CIDR ranges) configured in Portal → API Security. When the allowlist is in enforce mode, requests from any other IP are rejected with:
HTTP/1.1 403 Forbidden
{
"error": {
"code": "ip_not_allowed",
"message": "Source IP is not in this merchant's API allowlist",
"request_id": "req_01HF..."
}
}A monitor mode is also available — denials are logged to Portal → API Security → Denials but requests still succeed, so you can validate the list before flipping to enforce. The hosted checkout (/v1-checkout/*) is never IP-restricted — it must work from any customer's browser.
Idempotency
Mutating endpoints accept an Idempotency-Key header (any opaque string up to 255 chars — a UUID v4 is recommended). Replays with the same key and identical body return the original response; replays with the same key and a different body return a 409 idempotency_conflict.
Idempotency-Key: 7c2b6e0a-2f4a-4a8e-9a2b-1234567890abPayment sessions
Create a new payment session and obtain a hosted checkout URL.
Request body
| field | type | required | description |
|---|---|---|---|
mid_code | string | Yes | MID identifier configured for your account. |
merchant_order_id | string | Yes | Your unique order reference. Must be unique per merchant. |
amount_paise | integer | Yes | Amount in the smallest currency unit (e.g. paise). |
currency | string | Yes | ISO-4217 currency code. Currently INR. |
customer | object | No | Buyer details. See customer object below. Sending richer customer data materially improves risk scoring and approval rates. |
metadata | object | No | String → string map (max 20 keys, key ≤ 64 chars, value ≤ 500 chars). Forwarded to our risk engine as custom fields — use it for cart_id, source, promo_code, loyalty_tier, etc. |
description | string | No | Statement / checkout description (≤ 500 chars). |
return_url | string (URL) | No | Where the customer is sent after checkout. |
expires_in_seconds | integer | No | Session TTL. Default 900, max 3600. |
Customer object
All fields are optional. Fields marked (risk) are passed to our risk engine and improve approval rates / chargeback protection.
| field | type | required | description |
|---|---|---|---|
customer.id | string | No | (risk) Your stable customer reference — links sessions to a returning buyer. |
customer.name | string | No | Buyer's full name. |
customer.email | string | No | (risk) Used for email reputation & digital-footprint lookup. |
customer.phone | string | No | (risk) Used for phone reputation lookup. |
customer.created_at | string (ISO 8601) | No | (risk) When the buyer's account was created on your platform — account-age signal. |
customer.address.line1 | string | No | (risk) Billing address line 1. |
customer.address.line2 | string | No | (risk) Billing address line 2. |
customer.address.city | string | No | (risk) Billing city. |
customer.address.region | string | No | (risk) Billing state / region. |
customer.address.postal_code | string | No | (risk) Billing postal / ZIP code. |
customer.address.country | string | No | (risk) ISO country (e.g. IN, US). |
Note: the buyer's ip_address, user_agent, and device fingerprint are captured by our hosted checkout page automatically. You do not send them in the API call.
Example request
curl -X POST https://api.uplinqgateway.com/v1/v1-sessions \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"mid_code": "default",
"merchant_order_id": "ORD-2026-0001",
"amount_paise": 49900,
"currency": "INR",
"customer": {
"id": "cust_8821",
"name": "Asha K",
"email": "asha@example.com",
"phone": "+919812345678",
"created_at": "2024-03-12T08:14:00Z",
"address": {
"line1": "12 MG Road",
"city": "Bengaluru",
"region": "KA",
"postal_code": "560001",
"country": "IN"
}
},
"metadata": {
"cart_id": "CART-8821",
"source": "mobile_app",
"promo_code": "SUMMER25"
},
"description": "Order ORD-2026-0001",
"return_url": "https://merchant.com/orders/ORD-2026-0001"
}'Example response — 201 Created
{
"session_id": "9c0a8e1e-2f4a-4a8e-9a2b-7e3b5a4f0c11",
"hosted_url": "https://uplinqgateway.com/c/aB3kZ9pQwL2yUvX7nJ4hM8rT",
"status": "active",
"expires_at": "2026-05-06T18:34:00.000Z",
"amount_paise": 49900,
"currency": "INR"
}Retrieve a session and its latest transaction attempt.
curl https://api.uplinqgateway.com/v1/v1-sessions/9c0a8e1e-2f4a-4a8e-9a2b-7e3b5a4f0c11 \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx"{
"id": "9c0a8e1e-2f4a-4a8e-9a2b-7e3b5a4f0c11",
"merchant_order_id": "ORD-2026-0001",
"amount_paise": 49900,
"currency": "INR",
"status": "succeeded",
"description": "Order ORD-2026-0001",
"return_url": "https://merchant.com/orders/ORD-2026-0001",
"expires_at": "2026-05-06T18:34:00.000Z",
"created_at": "2026-05-06T18:19:00.000Z",
"transaction": {
"id": "5b1...",
"status": "succeeded",
"payment_method": "upi_intent",
"provider": "simulator",
"provider_txn_id": "SIM_XXXX"
}
}Hosted checkout (public)
These endpoints power the hosted checkout page and are called from the browser. They are scoped to a single session token and require no API key.
curl https://api.uplinqgateway.com/v1/v1-checkout/aB3kZ9pQwL2yUvX7nJ4hM8rTStart a payment attempt for the session.
curl -X POST https://api.uplinqgateway.com/v1/v1-checkout/aB3kZ9pQwL2yUvX7nJ4hM8rT/initiate \
-H "Content-Type: application/json" \
-d '{ "method": "upi_intent" }'{
"txn_id": "5b1f...",
"intent_url": "upi://pay?pa=...&am=499.00&cu=INR&tr=SIM_XXXX",
"qr_string": null
}Poll for the latest session and transaction state.
{
"session_status": "succeeded",
"transaction": { "id": "5b1f...", "status": "succeeded" }
}Transactions
curl https://api.uplinqgateway.com/v1/v1-transactions/5b1f... \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx"{
"id": "5b1f...",
"session_id": "9c0a...",
"status": "succeeded",
"payment_method": "upi_intent",
"provider": "simulator",
"provider_txn_id": "SIM_XXXX",
"amount_paise": 49900,
"currency": "INR",
"created_at": "2026-05-06T18:20:01.000Z",
"updated_at": "2026-05-06T18:20:08.000Z"
}Status values: initiated, pending, succeeded, failed, cancelled.
Refunds
curl -X POST https://api.uplinqgateway.com/v1/v1-refunds \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"transaction_id": "5b1f...",
"amount_paise": 49900,
"reason": "customer_request"
}'{
"id": "rf_8a2c...",
"transaction_id": "5b1f...",
"status": "pending",
"amount_paise": 49900,
"currency": "INR",
"created_at": "2026-05-06T18:30:00.000Z"
}Status values: initiated, pending, succeeded, failed.
Payouts
Send bank payouts to vendors, suppliers, or end-users via NEFT, IMPS or RTGS. You can either pre-register a beneficiary and reuse its id, or pass the bank details inline on the payout itself — we'll auto-create (or reuse) the beneficiary for you and return its beneficiary_id in the response so you can store it for future calls.
On create we debit amount + fee + GST from your available balance and hold it. The UTR is returned once the bank rail confirms the transfer. If you have dual approval enabled and the amount is at or above your threshold, the payout is held in approval_state: "pending" until two admins approve it in the portal — it is never submitted to the bank until then.
Option A — One-shot payout (inline beneficiary)
Easiest path: send the bank details directly on the payout. If we've seen this (account_number, ifsc) for you before, we reuse the existing beneficiary; otherwise we create one in the background. The returned beneficiary_id can be reused on subsequent payouts (Option B) to skip the inline block.
curl -X POST https://api.uplinqgateway.com/v1/v1-payouts \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount_paise": 50000,
"payment_mode": "IMPS",
"merchant_reference": "PO-2026-0042",
"beneficiary": {
"account_name": "Acme Pvt Ltd",
"account_number": "50100123456789",
"ifsc": "HDFC0001234",
"bank_name": "HDFC Bank",
"contact_name": "Acme AP",
"contact_email": "ap@acme.example",
"contact_phone": "+919876543210"
}
}'Option B — Pre-register a beneficiary, then pay
1. Create a beneficiary
curl -X POST https://api.uplinqgateway.com/v1/v1-beneficiaries \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"account_name": "Acme Pvt Ltd",
"account_number": "50100123456789",
"ifsc": "HDFC0001234",
"bank_name": "HDFC Bank",
"contact_name": "Acme AP",
"contact_email": "ap@acme.example",
"contact_phone": "+919876543210"
}'{
"id": "b1f9a3c4-...-...-...",
"account_name": "Acme Pvt Ltd",
"account_number_last4": "6789",
"ifsc": "HDFC0001234",
"bank_name": "HDFC Bank",
"status": "active",
"currency": "INR",
"created_at": "2026-06-04T10:11:12.000Z"
}For security, only the last 4 digits of the account number are returned. GET /v1-beneficiaries lists active beneficiaries; DELETE /v1-beneficiaries/{id} archives one.
2. Quote the fees (optional)
curl -X POST https://api.uplinqgateway.com/v1/v1-payouts/fee-quote \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "amount_paise": 50000, "payment_mode": "IMPS" }'{
"amount_paise": 50000,
"mode_fee_paise": 500,
"mdr_fee_paise": 250,
"fee_paise": 750,
"gst_paise": 135,
"total_deducted_paise": 50885,
"beneficiary_receives_paise": 50000,
"currency": "INR",
"payment_mode": "IMPS"
}3. Create the payout (by beneficiary id)
curl -X POST https://api.uplinqgateway.com/v1/v1-payouts \
-H "Authorization: Bearer kid_live_abc:sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"beneficiary_id": "b1f9a3c4-...-...-...",
"amount_paise": 50000,
"payment_mode": "IMPS",
"merchant_reference": "PO-2026-0042"
}'{
"id": "p0a1b2c3-...-...-...",
"status": "submitted",
"approval_state": "not_required",
"amount_paise": 50000,
"fee_paise": 750,
"gst_paise": 135,
"total_deducted_paise": 50885,
"beneficiary_receives_paise": 50000,
"currency": "INR",
"payment_mode": "IMPS",
"beneficiary_id": "b1f9a3c4-...-...-...",
"merchant_reference": "PO-2026-0042",
"provider_ref": "PH_98765",
"utr": null,
"created_at": "2026-06-04T10:12:00.000Z",
"submitted_at": "2026-06-04T10:12:01.000Z"
}Returns 202 Accepted instead of 201 when the payout requires dual approval — in that case status stays queued and approval_state is pending until approved in the portal.
4. Poll for completion
Lifecycle: queued → submitted → succeeded (UTR populated) / failed (held balance auto-released, see failure_reason) / reversed. Poll every 30–60s; final state is usually reached within 2–5 minutes for IMPS, longer for NEFT batch windows.
Sandbox testing
In the sandbox environment, payouts route to GSX PayHub sandbox. Use any valid-format IFSC (e.g. HDFC0001234) and any 10–16 digit account number — the sandbox does not deposit real funds. The same Authorization header you use for sessions, transactions and refunds works for payouts; no extra credentials needed.
Webhooks
Configure a webhook URL and secret for your merchant in the portal. Uplinq POSTs a JSON payload to that URL on every meaningful state transition, signs it with HMAC-SHA256, and retries with exponential backoff (1m → 5m → 30m → 2h → 12h, up to 6 attempts) until you respond 2xx.
Headers
Content-Type: application/json
x-uplinq-event: transaction.updated
x-uplinq-signature: t=1746551400,v1=4f8...The event ID, delivery timestamp and resource IDs are inside the JSON body (id, created_at, data). The signed string is <t>.<raw_body>.
Event types
transaction.updated— fired on every transaction status change (initiated → pending → succeeded / failed / cancelled).refund.updated— fired on every refund status change.session.expired— fired when a session passesexpires_atwithout reaching a terminal state.
Read the session/transaction status from the payload — there is no separate session.succeeded event. A successful payment surfaces as transaction.updated with data.status = "succeeded".
Example payload
{
"id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
"type": "transaction.updated",
"created_at": "2026-05-06T18:20:08.000Z",
"data": {
"id": "5b1f...",
"session_id": "9c0a...",
"merchant_order_id": "ORD-2026-0001",
"status": "succeeded",
"payment_method": "upi_intent",
"provider": "gsx",
"provider_txn_id": "GSX_XXXX",
"amount_paise": 49900,
"currency": "INR"
}
}Verifying the signature (Node.js)
import crypto from "node:crypto";
// header = req.headers["x-uplinq-signature"] // "t=1746551400,v1=4f8..."
// rawBody = the exact request body bytes (do NOT JSON.parse + re-stringify)
export function verifyUplinqSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("="))
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(parts.v1, "hex"),
Buffer.from(expected, "hex")
);
}Reject events whose t is older than 5 minutes. Treat the handler as idempotent — the same event id may be delivered more than once.
Error model
All errors share the same envelope:
{
"error": {
"code": "validation_error",
"message": "merchant_order_id already used",
"request_id": "req_01HF..."
}
}| code | http | meaning |
|---|---|---|
authentication_error | 401 | Missing or invalid API key. |
validation_error | 400 | Request body or parameters were invalid. |
idempotency_conflict | 409 | Idempotency key reused with a different body. |
session_inactive | 409 | Session is no longer in an actionable state. |
not_found | 404 | Resource does not exist or is not visible to you. |
rate_limited | 429 | Too many requests. Back off and retry. |
internal_error | 500 | Something went wrong on our side. Safe to retry. |
Testing with the simulator
Every account ships with a simulator provider that mimics a real acquirer with configurable delays and failure rates. Use it end-to-end before connecting a live provider:
- Create a session against the simulator MID (
mid_code: "default"). - Open the returned
hosted_urlin a browser. - Initiate the payment; the simulator transitions to
succeededafter a few seconds. - Your webhook endpoint receives
session.succeeded.