Production hosts

API base URL: https://nineteenapis.online — use this for all /api/, /pay/, and /webhooks/ calls.

Documentation only: https://public.nineteenapis.online — this host serves these HTML pages and Swagger; it does not proxy API traffic.

API V2 — HMAC integration

Paths include /api/v2/. You must sign the raw JSON body and whitelist caller IPs.

V2 — HMAC authentication

Required headers (exactly these four — no others are recognized)

  • X-API-KEY
  • X-Timestamp
  • X-Nonce
  • X-Signature

Common mistakes

  • Do not send X-NP-KEY — it is not recognized.
  • Do not send X-Method — it is not recognized.
  • Header names are case-insensitive in HTTP; the canonical names above match our examples.
  • Signature = HMAC-SHA256( pipeSecret, apiKey + timestamp + nonce + JSON.stringify(body) ) → lowercase hex.

Concatenate and sign with your pipe secret (same value as the salt for your API key in the dashboard):

stringToSign = apiKey + timestampSeconds + nonce + JSON.stringify(requestBody)
signature = HMAC_SHA256( secret, stringToSign ) → lowercase hex
  • X-API-KEY — must match the key you sign with (first segment of stringToSign).
  • X-Timestamp — Unix time in seconds (string); default allowed skew is ±300s from server time (operations may configure a different window server-side).
  • X-Nonce — unique per request (replay rejected).
  • X-Signature — hex digest; body bytes must be exactly what you put in JSON.stringify.
  • Optional X-Idempotency-Key on mutating routes (collect, refund, transfer, …).
  • Responses under /api/v2/* include X-Server-Time (Unix seconds) on every reply for clock reference.
  • If the timestamp is outside the allowed window you receive 401 with the usual error envelope; the JSON may also include serverTime, serverTimeISO, and skewSeconds — see clock skew below.

V2 requires at least one whitelisted IP for the pipe or requests return 403.

Clock skew (HMAC timestamps)

For drifting device clocks, call GET /api/v2/time (no auth), compare to local Unix time, store an offset, and sign with X-Timestamp adjusted by that offset. You can also read X-Server-Time from any prior V2 response.

Example 401 JSON when the timestamp is outside the window (message string unchanged for existing SDKs):

{"success":false,"error":{"message":"Request timestamp outside allowed window","statusCode":401,"serverTime":1715123456,"serverTimeISO":"2024-05-08T06:30:56.000Z","skewSeconds":-362}}

cURL + OpenSSL (bash)

Set NP_KEY and NP_SALT, then build the same JSON string for -d and for signing.

export NP_KEY="your_api_key"
export NP_SALT="your_salt"
BODY='{"payload":{"PAY_ID":"PAY123","ORDER_ID":"ORD456","AMOUNT":"10000","CURRENCY_CODE":"356","TXNTYPE":"SALE","MOP_TYPE":"UPI","PAYMENT_TYPE":"UPI","CUST_EMAIL":"a@b.com","CUST_PHONE":"9999999999"},"environment":"prod"}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
SIG=$(printf '%s' "${NP_KEY}${TS}${NONCE}${BODY}" | openssl dgst -sha256 -hmac "${NP_SALT}" | awk '{print $2}')
curl -sS -X POST 'https://nineteenapis.online/api/v2/payments/collect' \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ${NP_KEY}" \
  -H "X-Timestamp: ${TS}" \
  -H "X-Nonce: ${NONCE}" \
  -H "X-Signature: ${SIG}" \
  -H "X-Idempotency-Key: my-idempotency-key-1" \
  -d "${BODY}"

Node.js

const crypto = require('crypto');

function v2SignAndHeaders(bodyObject, apiKey, salt) {
  const bodyStr = JSON.stringify(bodyObject);
  const ts = String(Math.floor(Date.now() / 1000));
  const nonce = crypto.randomBytes(16).toString('hex');
  const payload = apiKey + ts + nonce + bodyStr;
  const signature = crypto.createHmac('sha256', salt).update(payload).digest('hex');
  return {
    headers: {
      'Content-Type': 'application/json',
      'X-API-KEY': apiKey,
      'X-Timestamp': ts,
      'X-Nonce': nonce,
      'X-Signature': signature,
    },
    bodyStr,
  };
}

// Example (Node 18+; run inside async or use .mjs)
(async () => {
  const { headers, bodyStr } = v2SignAndHeaders(
    { payload: { PAY_ID: 'PAY123', ORDER_ID: 'ORD456', AMOUNT: '10000', CURRENCY_CODE: '356',
        TXNTYPE: 'SALE', MOP_TYPE: 'UPI', PAYMENT_TYPE: 'UPI', CUST_EMAIL: 'a@b.com', CUST_PHONE: '9999999999' },
      environment: 'prod' },
    process.env.NP_KEY,
    process.env.NP_SALT
  );
  const res = await fetch('https://nineteenapis.online/api/v2/payments/collect', {
    method: 'POST',
    headers: { ...headers, 'X-Idempotency-Key': 'unique-key-123' },
    body: bodyStr,
  });
  console.log(await res.json());
})();

Python

Use the same JSON serialization as the wire body (e.g. json.dumps(..., separators=(',', ':'))) so the signature matches.

import hmac, hashlib, json, os, time, secrets, urllib.request

def v2_headers(body: dict, api_key: str, salt: str):
    body_str = json.dumps(body, separators=(",", ":"))
    ts = str(int(time.time()))
    nonce = secrets.token_hex(16)
    payload = (api_key + ts + nonce + body_str).encode()
    sig = hmac.new(salt.encode(), payload, hashlib.sha256).hexdigest()
    return {
        "Content-Type": "application/json",
        "X-API-KEY": api_key,
        "X-Timestamp": ts,
        "X-Nonce": nonce,
        "X-Signature": sig,
    }, body_str

body = {"payload": {"PAY_ID": "PAY123", "ORDER_ID": "ORD456", "AMOUNT": "10000", "CURRENCY_CODE": "356",
    "TXNTYPE": "SALE", "MOP_TYPE": "UPI", "PAYMENT_TYPE": "UPI", "CUST_EMAIL": "a@b.com", "CUST_PHONE": "9999999999"},
    "environment": "prod"}
h, body_str = v2_headers(body, os.environ["NP_KEY"], os.environ["NP_SALT"])
h["X-Idempotency-Key"] = "unique-key-123"
req = urllib.request.Request(
    "https://nineteenapis.online/api/v2/payments/collect",
    data=body_str.encode(),
    headers=h,
    method="POST",
)
with urllib.request.urlopen(req) as r:
    print(r.read().decode())
GET /api/v2/time

Server time (public)

No HMAC or API key. Use for a one-time clock sync before signing V2 requests. The response header X-Server-Time matches serverTime in the body.

cURL

curl -sS 'https://nineteenapis.online/api/v2/time'

Node.js

(async () => {
  const res = await fetch('https://nineteenapis.online/api/v2/time');
  console.log('X-Server-Time', res.headers.get('x-server-time'));
  console.log(await res.json());
})();

Python

import json, urllib.request
req = urllib.request.Request('https://nineteenapis.online/api/v2/time', method='GET')
with urllib.request.urlopen(req) as r:
    print('X-Server-Time', r.headers.get('X-Server-Time'))
    print(json.loads(r.read().decode()))

Typical success (200)

{"success":true,"serverTime":1715123456,"serverTimeISO":"2024-05-08T06:30:56.000Z"}

V1 collect — gateway payload (API V2 HMAC)

Routes such as /api/v2/payments/collect use a payload object (field names similar to Native V1). This is not Native V2 — Native V2 is only /api/v2/payments/np-native/*. HMAC headers; idempotency on collect/refund where the route supports it.

POST /api/v2/payments/collect

Collect

X-Idempotency-Key (optional): include on mutating requests for safe retries; recommended for collect/refund/transfer where supported.

Typical success (200)

{"success":true,"transactionId":"...","status":"PENDING","intentUrl":"upi://...","raw":{},"decrypted":{}}
POST /api/v2/payments/validate-vpa

Validate VPA

Typical success (200)

{"success":true,"raw":{},"decrypted":{}}
POST /api/v2/payments/status

Status

Typical success (200)

{"success":true,"status":"SUCCESS","raw":{},"decrypted":{}}
POST /api/v2/payments/refund

Refund

X-Idempotency-Key (optional).

Typical success (200)

{"success":true,"status":"PENDING","raw":{},"decrypted":{}}
POST /api/v2/payments/static-qr

Static QR

Typical success (200)

{"success":true,"raw":{},"decrypted":{}}

API V2 — Native V2

Paths: /api/v2/payments/np-native/collect and /api/v2/payments/np-native/status only. This is Native V2 — separate from Native V1 and separate from the V1 collect API above. Requires the Native V2 pipe, onboarding, and VPA. Returns checkoutUrl, scanAndPayUrl (minimal QR + download page for gallery scan), share_intentURL (Web Share page: share QR image to UPI apps), upiDeeplink, optional collectIntent, pgUrl (always requested server-side), and collectVpaUrl (reserved; currently null).

POST /api/v2/payments/np-native/collect

Native V2 — collect (deeplink + checkout)

X-Idempotency-Key (optional): recommended for safe retries.

Request body (JSON)

FieldRequiredDescription
amountYesAmount in INR (positive number).
orderIdYesYour order reference (unique per payment attempt).
customerNameNoOptional display.
noteNoShown as UPI note (tn) when supported.
includePgFallbackNoDeprecated — ignored. Hosted PG URL is always requested server-side; you do not need to send this field.

Response (200) — key fields

FieldDescription
checkoutUrlHTTPS URL of the TSP-hosted checkout page for the payer. Redirect or link the customer here; on mobile they pick a UPI app; on desktop they scan the QR. Same payment and webhooks as other flows.
upiDeeplinkStandard upi://pay?… string for the primary merchant VPA — for embedding in your own QR.
collectIntentOptional upi://pay?… with the same tr/tn as upiDeeplink, but payee is the collect UPI ID from virtual account data (if present). For diagnostics only — your payment handler may still treat app-opened flows as restricted.
scanAndPayUrlHTTPS URL of a minimal page that renders the payment QR, auto-downloads a PNG, and includes a save button. Customer opens their UPI app, uses scan from gallery — typically classified as QR mode by the network.
share_intentURLHTTPS URL of a page that builds the payment QR in memory and uses the Web Share API to share the QR image to a UPI app (e.g. Google Pay, PhonePe). Useful when deeplink upi:// is blocked by the handler but QR mode is allowed.
pgUrlHosted PG URL from the provider when available (always requested; may be null if generation fails).
collectVpaUrlReserved for UPI Collect (pull to payer VPA). Currently always null — not available from this handler.
expiresAtISO time; checkout session aligns with this window.

Typical success (200)

{"success":true,"transactionId":"...","orderId":"order-abc-001","upiDeeplink":"upi://pay?...","collectIntent":null,"checkoutUrl":"https://nineteenapis.online/pay/TXN...","scanAndPayUrl":"https://nineteenapis.online/pay/scan/TXN...","share_intentURL":"https://nineteenapis.online/pay/share/TXN...","pgUrl":null,"collectVpaUrl":null,"expiresAt":"..."}

Hosted checkout (public payer pages)

No HMAC — these URLs are for end customers after you receive checkoutUrl from collect. Base host matches your deployment (e.g. production API host).

  • GET /pay/:transactionId — HTML checkout (UPI app options on mobile, QR on desktop).
  • GET /pay/scan/:transactionId — HTML scan-and-pay (large QR, auto-download PNG, save button; gallery scan flow).
  • GET /pay/share/:transactionId — HTML share-to-UPI (Web Share API: share QR image to UPI app).
  • GET /pay/data/:transactionId — JSON for the page (amount, merchant name, upiDeeplink, scanAndPayUrl, share_intentURL, optional collectUpiId / collectIntent, expiry).
  • POST /pay/pg-url/:transactionId — JSON {"success":true,"pgUrl":"…"|null} to fetch hosted PG URL on demand for the checkout page.
  • GET /pay/status/:transactionId — JSON {"success":true,"status":"PENDING"|"SUCCESS"|…} for polling.
  • POST /pay/event — optional anonymous analytics from the checkout page (body: transactionId, eventType, optional data). No secrets.

Merchant outbound webhooks after payment success are unchanged — see outbound webhooks (HMAC X-TSP-Signature).

POST /api/v2/payments/np-native/status

Native V2 — status

Look up one payment by merchant orderId and/or TSP transactionId, or batch up to 50 combined references via orderIds / transactionIds. Do not mix single fields (orderId/transactionId) with batch arrays in the same request.

FieldRequiredNotes
orderIdoptionalSingle lookup only (with single mode).
transactionIdoptionalSingle lookup only (with single mode).
orderIdsoptionalBatch: array of merchant order references; trimmed, de-duplicated; max 50 combined with transactionIds.
transactionIdsoptionalBatch: array of TSP transaction IDs; same limits as orderIds.

Single ID — examples

Typical success (200) — single

{"success":true,"transactionId":"...","orderId":"...","status":"PENDING","amount":"99.50","bankReference":null,"paymentMethod":null,"createdAt":"...","updatedAt":"..."}

Batch IDs — examples (same HMAC rules; body uses arrays)

Typical success (200) — batch

{"success":true,"data":[{"requestedId":"order-abc-001","kind":"orderId","status":"SUCCESS","transactionId":"TXN...","orderId":"order-abc-001","amount":"99.50","bankReference":null,"paymentMethod":null,"createdAt":"...","updatedAt":"..."},{"requestedId":"order-abc-002","kind":"orderId","status":"PENDING","transactionId":"TXN...","orderId":"order-abc-002","amount":"50.00","bankReference":null,"paymentMethod":null,"createdAt":"...","updatedAt":"..."},{"requestedId":"missing-order","kind":"orderId","status":"NOT_FOUND"}]}

V2 — JPSL

POST /api/v2/payments/jpsl/initiate-sale

Initiate sale

Typical success (200)

{"success":true,"transactionId":"...","redirectURI":"https://...","tranCtx":"...","raw":{}}
POST /api/v2/payments/jpsl/generate-qr

Generate QR

Typical success (200)

{"success":true,"qrString":"upi://...","raw":{}}
POST /api/v2/payments/jpsl/status

Status

Typical success (200)

{"success":true,"status":"SUCCESS","raw":{}}
POST /api/v2/payments/jpsl/refund

Refund

X-Idempotency-Key (optional).

Typical success (200)

{"success":true,"status":"PENDING","raw":{}}

V2 — Paytm Escrow

POST /api/v2/payments/paytm/collect

Collect

Typical success (200)

{"success":true,"collectRef":"...","link":"https://...","raw":{}}
POST /api/v2/payments/paytm/status

Status

Batch status from stored transaction rows. Returns one item in data for every collect_ref in the request, in request order. Refs that don't match a stored transaction come back with status: "NOT_FOUND" and paid: false.

Response item shape (each entry in data[])

FieldTypeNotes
collect_refstringsnake_case. Echoes the requested ref.
transactionIdstringInternal NineteenPay txn id. Empty when NOT_FOUND.
statusstringOne of SUCCESS, PENDING, FAILED, NOT_FOUND.
paidbooleanPre-computed: status === 'SUCCESS'. Read this directly.
amountnumberIn INR. 0 when NOT_FOUND.
bankrefstring|nullUTR once paid; null otherwise.

Typical success (200)

{
  "success": true,
  "data": [
    {
      "collect_ref": "20260430174031TESTREF001",
      "transactionId": "TXN9F4A2B...",
      "status": "SUCCESS",
      "paid": true,
      "amount": 115,
      "bankref": "ICIC1234567890"
    }
  ],
  "raw": { "data": [ /* same array */ ] }
}

Integrator note: read data[i].paid directly. Refs you sent that don't match any stored transaction will come back with status: "NOT_FOUND" rather than being dropped silently.

V2 — NSDL UPI Pay-In

UPI intent-link collect via the same escrow-style flow as the row above. Requires HMAC V2 auth, IP whitelist, and your account enabled for pipe NSDL_PAYIN. Response may include gateway fields under raw.

POST /api/v2/payments/nsdl/collect

Collect

Create a collect / intent link for the configured payee.

HeaderRequiredDescription
X-API-KEYYesYour NineteenPay API key for this pipe
X-TimestampYesUnix epoch seconds (string)
X-NonceYesUnique nonce per request
X-SignatureYesHMAC-SHA256 per V2 auth
JSON body fieldRequiredType / notes
amountYesnumber
collect_refNostring; alphanumeric only (non-alphanumeric characters are stripped server-side). Max 30 chars after sanitization.
user_refNostring; min 5 chars, max 50 chars. Defaults to Customer if omitted or shorter than 5.
display_nameNostring
transaction_noteNostring

Typical success (200)

{"success":true,"transactionId":"...","collectRef":"...","status":"...","crn":null,"link":"https://...","feeAccountBalance":null,"raw":{}}
POST /api/v2/payments/nsdl/status

Status (database)

Batch status from stored transaction rows (same headers as collect). Returns one item in data for every collect_ref in the request, in request order.

JSON body fieldRequiredType / notes
collect_ref_arrYesarray of strings (collect references), at least one entry, each non-empty

Response item shape (each entry in data[])

FieldTypeNotes
collect_refstringsnake_case. Echoes the requested ref.
transactionIdstringInternal NineteenPay txn id. Empty string when status is NOT_FOUND.
statusstringOne of SUCCESS, PENDING, FAILED, NOT_FOUND.
paidbooleanPre-computed: status === 'SUCCESS'. Read this directly instead of comparing strings.
amountnumberIn INR. 0 when status is NOT_FOUND.
bankrefstring|nullBank reference (UTR) once paid; null otherwise.

Typical success (200)

{
  "success": true,
  "data": [
    {
      "collect_ref": "20260430174031TESTREF001",
      "transactionId": "TXN9F4A2B...",
      "status": "SUCCESS",
      "paid": true,
      "amount": 115,
      "bankref": "ICIC1234567890"
    },
    {
      "collect_ref": "REF_THAT_DOES_NOT_EXIST",
      "transactionId": "",
      "status": "NOT_FOUND",
      "paid": false,
      "amount": 0,
      "bankref": null
    }
  ],
  "raw": { "data": [ /* same array */ ] }
}

Integrator note: read data[i].paid directly. Avoid deriving "paid" from any other field — status values may grow over time. Refs you sent that don't match any stored transaction will come back with status: "NOT_FOUND" rather than being dropped silently.

V2 — Payouts

POST /api/v2/payouts/balance

Balance

Typical success (200)

{"success":true,"balance":123.45,"raw":{}}
POST /api/v2/payouts/verify-account

Verify account

Typical success (200)

{"success":true,"status":"SUCCESS","beneficiaryName":"...","raw":{}}
POST /api/v2/payouts/transfer

Transfer

X-Idempotency-Key (optional).

Typical success (200)

{"success":true,"transactionId":"...","status":"PENDING","raw":{}}
POST /api/v2/payouts/status

Status

Typical success (200)

{"success":true,"status":"SUCCESS","raw":{}}

V2 — Outbound webhooks (NineteenPay → your callback URL)

When a signing secret is configured on your pipe, we send:

  • X-TSP-Timestamp — milliseconds since epoch (string)
  • X-TSP-SignatureHMAC_SHA256(secret, timestamp + "." + JSON.stringify(body)) hex

Verify (Node.js)

const crypto = require('crypto');
function verifyTspWebhook(body, tsHeader, sigHeader, signingSecret) {
  const base = tsHeader + '.' + JSON.stringify(body);
  const expected = crypto.createHmac('sha256', signingSecret).update(base).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(sigHeader, 'utf8'));
}

Verify (Python)

import hmac, hashlib, json
def verify_tsp(body: dict, ts: str, sig: str, secret: str) -> bool:
    base = ts + "." + json.dumps(body, separators=(",", ":"))
    exp = hmac.new(secret.encode(), base.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(exp, sig)

Payload shapes (examples)

Gateway passthrough (Native V1 / V1 collect) — forwarded gateway fields (ORDER_ID, STATUS, RRN, HASH, …).
Escrow channel (normalized) — transactionId, orderId, collect_ref, status, amount, bankref, crn, upi_transaction_id, payer_vpa, pipeCode. Same shape for NSDL UPI Pay-In (NSDL_PAYIN).
JPSL — raw S2S JSON as received.
Payout rail (raw) — forwarded JSON (ClientOrderId, StatusCode, UTR, Checksum, …).
Native V2 settlement — { "event":"payment.success", "transactionId", "orderId", "status":"SUCCESS", "amount", "bankReference", "channel", "pipeCode", "adhoc"?: true }