Overview

The Uplinq Payments API is a small, opinionated REST API for taking payments through any connected provider. The integration loop is:

  1. Create a payment session from your backend.
  2. Redirect the customer to the returned hosted checkout URL.
  3. 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

Base URLtext
https://api.uplinqgateway.com/v1

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

Header formatbash
Authorization: Bearer kid_live_abc123:sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Never 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:

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

bashbash
Idempotency-Key: 7c2b6e0a-2f4a-4a8e-9a2b-1234567890ab

Payment sessions

POST/v1-sessions

Create a new payment session and obtain a hosted checkout URL.

Request body

fieldtyperequireddescription
mid_codestringYesMID identifier configured for your account.
merchant_order_idstringYesYour unique order reference. Must be unique per merchant.
amount_paiseintegerYesAmount in the smallest currency unit (e.g. paise).
currencystringYesISO-4217 currency code. Currently INR.
customerobjectNoBuyer details. See customer object below. Sending richer customer data materially improves risk scoring and approval rates.
metadataobjectNoString → 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.
descriptionstringNoStatement / checkout description (≤ 500 chars).
return_urlstring (URL)NoWhere the customer is sent after checkout.
expires_in_secondsintegerNoSession 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.

fieldtyperequireddescription
customer.idstringNo(risk) Your stable customer reference — links sessions to a returning buyer.
customer.namestringNoBuyer's full name.
customer.emailstringNo(risk) Used for email reputation & digital-footprint lookup.
customer.phonestringNo(risk) Used for phone reputation lookup.
customer.created_atstring (ISO 8601)No(risk) When the buyer's account was created on your platform — account-age signal.
customer.address.line1stringNo(risk) Billing address line 1.
customer.address.line2stringNo(risk) Billing address line 2.
customer.address.citystringNo(risk) Billing city.
customer.address.regionstringNo(risk) Billing state / region.
customer.address.postal_codestringNo(risk) Billing postal / ZIP code.
customer.address.countrystringNo(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

bashbash
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

jsonjson
{
  "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"
}
GET/v1-sessions/{session_id}

Retrieve a session and its latest transaction attempt.

bashbash
curl https://api.uplinqgateway.com/v1/v1-sessions/9c0a8e1e-2f4a-4a8e-9a2b-7e3b5a4f0c11 \
  -H "Authorization: Bearer kid_live_abc:sk_live_xxx"
jsonjson
{
  "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.

GET/v1-checkout/{token}
bashbash
curl https://api.uplinqgateway.com/v1/v1-checkout/aB3kZ9pQwL2yUvX7nJ4hM8rT
POST/v1-checkout/{token}/initiate

Start a payment attempt for the session.

bashbash
curl -X POST https://api.uplinqgateway.com/v1/v1-checkout/aB3kZ9pQwL2yUvX7nJ4hM8rT/initiate \
  -H "Content-Type: application/json" \
  -d '{ "method": "upi_intent" }'
jsonjson
{
  "txn_id": "5b1f...",
  "intent_url": "upi://pay?pa=...&am=499.00&cu=INR&tr=SIM_XXXX",
  "qr_string": null
}
GET/v1-checkout/{token}/state

Poll for the latest session and transaction state.

jsonjson
{
  "session_status": "succeeded",
  "transaction": { "id": "5b1f...", "status": "succeeded" }
}

Transactions

GET/v1-transactions/{txn_id}
bashbash
curl https://api.uplinqgateway.com/v1/v1-transactions/5b1f... \
  -H "Authorization: Bearer kid_live_abc:sk_live_xxx"
jsonjson
{
  "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

POST/v1-refunds
bashbash
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"
  }'
jsonjson
{
  "id": "rf_8a2c...",
  "transaction_id": "5b1f...",
  "status": "pending",
  "amount_paise": 49900,
  "currency": "INR",
  "created_at": "2026-05-06T18:30:00.000Z"
}
GET/v1-refunds/{refund_id}

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.

POST/v1-payouts
bashbash
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

POST/v1-beneficiaries
bashbash
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"
  }'
jsonjson
{
  "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)

POST/v1-payouts/fee-quote
bashbash
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" }'
jsonjson
{
  "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)

POST/v1-payouts
bashbash
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"
  }'
jsonjson
{
  "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

GET/v1-payouts/{payout_id}

Lifecycle: queuedsubmitted 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

texttext
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 passes expires_at without 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

jsonjson
{
  "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)

javascriptjavascript
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:

jsonjson
{
  "error": {
    "code": "validation_error",
    "message": "merchant_order_id already used",
    "request_id": "req_01HF..."
  }
}
codehttpmeaning
authentication_error401Missing or invalid API key.
validation_error400Request body or parameters were invalid.
idempotency_conflict409Idempotency key reused with a different body.
session_inactive409Session is no longer in an actionable state.
not_found404Resource does not exist or is not visible to you.
rate_limited429Too many requests. Back off and retry.
internal_error500Something 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:

  1. Create a session against the simulator MID (mid_code: "default").
  2. Open the returned hosted_url in a browser.
  3. Initiate the payment; the simulator transitions to succeeded after a few seconds.
  4. Your webhook endpoint receives session.succeeded.
Need a key, integration help, or have feedback on the API? support@uplinqgateway.com