Integration guide
PaymentSwitch has three integration points. All requests go to https://api.pswitch.live and use JSON over HTTPS.
| # | What | Direction |
|---|---|---|
| 1 | Payments API: create a payment and get a UPI intent | You → PaymentSwitch |
| 2 | Status Check API: fetch a payment by transactionId | You → PaymentSwitch |
| 3 | Webhooks: final status delivered to your server | PaymentSwitch → You |
Overview
How a payment flows
Your server ──(1) POST /upi/intent──▶ PaymentSwitch ──▶ chosen gateway
│ │
Customer pays in their UPI app ◀──────────┘ (UPI intent) │
▼
Gateway ──── webhook ───▶ PaymentSwitch ──(3) signed webhook──▶ Your server
Your server ──(2) GET /transactions/{transactionId}──▶ PaymentSwitch (any time)
You integrate with PaymentSwitch only. Gateways report outcomes to PaymentSwitch, and PaymentSwitch delivers them to you, so you never build a separate webhook per gateway.
Two IDs for every order
| Field | Whose ID | Use it for |
|---|---|---|
transactionId | PaymentSwitch (switch) ID, created by us for every payment | The Status Check API, support requests, matching webhooks to orders. Always present. |
gatewayTransactionId | Gateway ID, issued by the gateway that processes the payment | Reconciling against your gateway’s dashboard and settlement reports. |
Both appear in the payment response, the status response and every webhook. Your own merchantOrderId is echoed back too.
Set up
- Register a merchant account and wait for approval.
- In the dashboard, add and enable your payment gateway(s) under Gateways.
- Under API Integration: copy your API key, generate your API secret (shown once, keep it on your server only), and set your Callback URL.
PaymentSwitch routes payments to the gateways you configured. Settlement, refunds and compliance remain between you and your gateway.
Authentication: signing requests
Every API request carries three headers:
| Header | Value |
|---|---|
X-PS-Key | Your API key (starts with psk_). |
X-PS-Timestamp | Current time in epoch milliseconds. Must be within 5 minutes of our clock. |
X-PS-Signature | Lowercase hex of HMAC-SHA256(apiSecret, signingString). |
The signing string is four parts joined by dots:
{timestamp}.{METHOD}.{path-and-query}.{sha256hex(body)}
Use the exact path and query string you send (for example /api/v1/upi/intent), and the SHA-256 of the exact body bytes. For a request without a body, hash the empty string. Each signature can be used once, so sign every retry with a fresh timestamp.
import crypto from 'node:crypto';
function sign(method, path, body, apiKey, apiSecret) {
const ts = Date.now().toString();
const bodyHash = crypto.createHash('sha256').update(body ?? '').digest('hex');
const signature = crypto.createHmac('sha256', apiSecret)
.update(`${ts}.${method}.${path}.${bodyHash}`).digest('hex');
return { 'X-PS-Key': apiKey, 'X-PS-Timestamp': ts, 'X-PS-Signature': signature,
'Content-Type': 'application/json' };
}
import hashlib, hmac, time
def sign(method, path, body: bytes, api_key, api_secret):
ts = str(int(time.time() * 1000))
body_hash = hashlib.sha256(body or b"").hexdigest()
sig = hmac.new(api_secret.encode(), f"{ts}.{method}.{path}.{body_hash}".encode(),
hashlib.sha256).hexdigest()
return {"X-PS-Key": api_key, "X-PS-Timestamp": ts, "X-PS-Signature": sig,
"Content-Type": "application/json"}
static String hex(byte[] b) { return java.util.HexFormat.of().formatHex(b); }
static Map<String,String> sign(String method, String path, String body, String key, String secret) throws Exception {
String ts = Long.toString(System.currentTimeMillis());
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(body.getBytes(UTF_8)));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(UTF_8), "HmacSHA256"));
String sig = hex(mac.doFinal((ts + "." + method + "." + path + "." + bodyHash).getBytes(UTF_8)));
return Map.of("X-PS-Key", key, "X-PS-Timestamp", ts, "X-PS-Signature", sig);
}
BODY='{"merchantOrderId":"ORD-12345","amount":1250.00,"customer":{"mobile":"9999999999","name":"A Customer"},"deviceId":"device-abc-123"}'
PATH_='/api/v1/upi/intent'
TS=$(python3 -c 'import time;print(int(time.time()*1000))')
BH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIG=$(printf '%s' "$TS.POST.$PATH_.$BH" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed 's/^.* //')
curl -X POST "https://api.pswitch.live$PATH_" -H "Content-Type: application/json" \
-H "X-PS-Key: $API_KEY" -H "X-PS-Timestamp: $TS" -H "X-PS-Signature: $SIG" -d "$BODY"
1. Payments API: create a payment
Creates a payment, chooses the best gateway for it, and returns a UPI intent link to open in the customer’s UPI app.
Request body
| Field | Type | Description |
|---|---|---|
merchantOrderId | string, required | Your unique order ID (max 64 chars: letters, digits, . _ - /). Used for idempotency. |
amount | number, required | Amount in INR, 1.00 to 10,000,000.00, up to 2 decimals. |
currency | string | INR (default). |
customer.mobile | string, required | 10-digit Indian mobile number. |
customer.name | string, required | Customer name. |
customer.email | string | Customer email. |
customer.address | string | Optional. |
deviceId | string, required | A stable identifier for the customer’s device. Used to recognise returning customers and detect unusual activity. |
There is no callback field on the request: your callback URL is part of your merchant configuration.
{
"merchantOrderId": "ORD-12345",
"amount": 1250.00,
"currency": "INR",
"customer": { "mobile": "9999999999", "email": "customer@example.com", "name": "Customer Name" },
"deviceId": "device-abc-123"
}
Response 201 Created
{
"success": true,
"transactionId": "PSMUYPQVPQ020001OTT0",
"gatewayTransactionId": "MG1A2B3C4D5E6F70",
"merchantOrderId": "ORD-12345",
"status": "PROCESSING",
"amount": 1250.00,
"currency": "INR",
"gateway": "GATEWAY_B",
"upiIntentUri": "upi://pay?pa=…&am=1250.00&cu=INR&tr=MG1A2B3C4D5E6F70",
"createdAt": "2026-10-08T10:15:30Z",
"replayed": false
}
Open upiIntentUri on the customer’s device. The payment is not complete until you receive a final status. Store transactionId with your order.
Safe retries
The call is idempotent on merchantOrderId. If you retry the same order with the same body (for example after a timeout), you get the original payment back with 200 and "replayed": true. Reusing an order ID with a different body returns 409 DUPLICATE_ORDER.
2. Status Check API
transactionId is the PaymentSwitch ID returned by the Payments API. Sign the request like any other (empty body).
{
"transactionId": "PSMUYPQVPQ020001OTT0",
"gatewayTransactionId": "MG1A2B3C4D5E6F70",
"merchantOrderId": "ORD-12345",
"status": "SUCCESS",
"amount": 1250.00,
"currency": "INR",
"gateway": "GATEWAY_B",
"failureCode": null,
"createdAt": "2026-10-08T10:15:30Z",
"updatedAt": "2026-10-08T10:16:02Z"
}
| Status | Meaning |
|---|---|
PROCESSING | Intent created; waiting for the customer and the gateway. |
SUCCESS | Paid. Final. |
FAILED | Not paid. Final. failureCode says why (for example BANK_DECLINED, EXPIRED). |
PENDING | The gateway has not confirmed an outcome yet. It can still become SUCCESS or FAILED. Do not fulfil yet. |
CANCELLED | Cancelled before payment. Final. |
An unknown transactionId, or one that belongs to another merchant, returns 404. Use webhooks as the primary signal and this API to confirm or recover.
3. Webhooks
Payment gateways send their notifications to PaymentSwitch. PaymentSwitch updates the payment and then distributes the result to your callback URL, the one you saved in the dashboard (API Integration → Callback URL), as an HTTPS POST.
POST https://yourstore.com/payments/callback
Content-Type: application/json
X-PS-Timestamp: 1791420890404
X-PS-Signature: 9f2c…
{
"event": "payment.succeeded",
"transactionId": "PSMUYPQVPQ020001OTT0",
"gatewayTransactionId": "MG1A2B3C4D5E6F70",
"merchantOrderId": "ORD-12345",
"status": "SUCCESS",
"amount": 1250.00,
"currency": "INR",
"gateway": "GATEWAY_B",
"occurredAt": "2026-10-08T10:16:02Z"
}
Events: payment.succeeded and payment.failed (the latter adds failureCode). Match on transactionId (switch ID); gatewayTransactionId is the gateway’s own reference.
Verify the signature
Compute HMAC-SHA256(apiSecret, timestamp + "." + rawBody) over the raw request body, hex-encode it, and compare it to X-PS-Signature in constant time. Reject requests whose timestamp is more than 5 minutes old.
const expected = crypto.createHmac('sha256', API_SECRET)
.update(`${req.headers['x-ps-timestamp']}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-ps-signature']));
Delivery and retries
- Reply with any
2xxstatus quickly. Anything else (or a timeout of 8 seconds) counts as a failure. - Failed deliveries are retried after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours, then marked failed. Recent deliveries are listed in the dashboard.
- Delivery is at least once. Deduplicate on
transactionIdand make your handler idempotent. - The callback URL must be a public HTTPS URL. Private and internal addresses are rejected when you save it.
- Only final outcomes are sent. A synchronous error from the Payments API (for example
NO_ELIGIBLE_GATEWAY) is returned in the response, not by webhook.
Errors
Errors share one shape, with the HTTP status reflecting the problem:
{ "success": false, "code": "NO_ELIGIBLE_GATEWAY", "message": "No eligible payment gateway is available.",
"details": { "transactionId": "…", "reasons": { "GATEWAY_A": ["DAILY_AMOUNT_LIMIT"] } }, "requestId": "…" }
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | Validation failed. details.fields names each problem field. |
| 401 | UNAUTHENTICATED | Missing or invalid signature, stale timestamp, or a replayed signature. |
| 403 | MERCHANT_INACTIVE | Your account is not active. |
| 404 | NOT_FOUND | Unknown transactionId. |
| 409 | DUPLICATE_ORDER / CONFLICT | Order ID reused with a different body, or still being processed (retry shortly). |
| 422 | NO_ELIGIBLE_GATEWAY | No gateway can take this payment (disabled, amount limits, usage limits). details.reasons lists why per gateway. |
| 422 | BLOCKED_BY_POLICY | Declined by a risk flag. details.reasons names the exact flag, for example USER_HIGH_VELOCITY. The payment was not sent to any gateway. |
| 429 | RATE_LIMITED | Too many requests. Back off and retry. |
| 502 | GATEWAY_ERROR | Every eligible gateway failed to create the payment. |
Include the requestId when contacting support.