Authentication & Security — Partner API
Everything on this page applies to acting on behalf of a merchant (AEPS, DMT, UPI) — calls that go through fintech-gateway. Your own account management (profile, balance, managed merchants, commission) is a separate, simpler flow — see Logging in for account management at the bottom.
Credentials
There's no separate signup or key-issuance step: every partner account already has exactly one apiKey and one SaltAESKey on its paymentSystemUser record, and those are your gateway credentials.
| Credential | Source | Used as |
|---|---|---|
X-Client-Id | your apiKey | request identity |
| AES-256-GCM key | your SaltAESKey — 64-char hex or 44-char base64, both decode to 32 bytes | body encryption |
| HMAC-SHA256 checksum secret | the same SaltAESKey, used as a raw string (not decoded) | request signing |
SaltAESKey intentionally does double duty as both the encryption key and the checksum secret — there's only ever one secret issued per account. fintech-gateway itself holds no credential store of its own; it resolves X-Client-Id on each cache miss by calling paymentSystem's internal credential-lookup endpoint.
Where calls go
Every call is POST {gatewayBaseUrl}/process/<upstream-path> (a small number of routes are GET, sent the same way) — the gateway strips /process and forwards to the identical sub-path on paymentSystem. There's no operation-name mapping: whatever path you send is the path paymentSystem receives, byte-for-byte.
POST /process/aeps/withdrawal → paymentSystem POST /aeps/withdrawal
POST /process/dmt/txn/v1/remitter/register → paymentSystem POST /dmt/txn/v1/remitter/register
POST /process/upi/txn/v1/GenerateQR → paymentSystem POST /upi/txn/v1/GenerateQR
| Environment | fintech-gateway |
|---|---|
| Production | https://api.bucksbox.in/gateway |
| Staging | https://api.bucksbox.in/gateway-stage |
| Local dev | http://localhost:3001 |
e.g. production POST /process/aeps/withdrawal is POST https://api.bucksbox.in/gateway/process/aeps/withdrawal; the same call in staging is POST https://api.bucksbox.in/gateway-stage/process/aeps/withdrawal. See Endpoint Reference for the full, generated list of every path with both environments spelled out.
Required headers
| Header | Value |
|---|---|
X-Client-Id | your apiKey |
X-Timestamp | unix epoch seconds (not ms), ±300s tolerance |
X-IV | base64, 12 random bytes |
X-Tag | base64, 16-byte GCM auth tag |
X-Checksum | hex HMAC-SHA256 (see below) |
X-Request-Id | UUID v4 — also your idempotency key |
Request encryption (AES-256-GCM)
- Serialize your plaintext JSON body:
rawBody = JSON.stringify(body). - Generate a random 12-byte IV.
- Build the AAD:
aad = "<clientId>.<timestamp>". - Encrypt with AES-256-GCM using your 32-byte key, the IV, and the AAD.
- Send
{ "payload": "<base64 ciphertext>" }as the body, withX-IVandX-Tagset from the encryption step.
import { createCipheriv, randomBytes } from 'crypto';
const key = Buffer.from(aesKeyHex, 'hex'); // or Buffer.from(aesKeyB64, 'base64') — both → 32 bytes
const iv = randomBytes(12);
const aad = `${clientId}.${timestamp}`;
const cipher = createCipheriv('aes-256-gcm', key, iv);
cipher.setAAD(Buffer.from(aad, 'utf8'));
const ciphertext = Buffer.concat([cipher.update(rawBody, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
const envelope = JSON.stringify({ payload: ciphertext.toString('base64') });
Checksum (HMAC-SHA256)
Sign the encrypted envelope, not your plaintext body
Checksum verification runs before decryption in the gateway's middleware chain. At the point your checksum is checked, the request body the gateway has in hand is still { "payload": "<ciphertext>" } — the canonical string is built from that JSON, not the plaintext you started with. Sign your rawBody instead of the final envelope and every call fails with 401 Checksum invalid.
canonical = "<timestamp>.<clientId>.<envelope>"
X-Checksum = HMAC-SHA256(canonical, checksumSecret).hex()
import { createHmac } from 'crypto';
const canonical = `${timestamp}.${clientId}.${envelope}`;
const checksum = createHmac('sha256', checksumSecret)
.update(canonical)
.digest('hex');
Putting it together
import { createCipheriv, createHmac, randomBytes, randomUUID } from 'crypto';
import axios from 'axios';
async function callPartnerApi(path, plaintextBody) {
const rawBody = JSON.stringify(plaintextBody);
const timestamp = Math.floor(Date.now() / 1000);
const iv = randomBytes(12);
const aad = `${clientId}.${timestamp}`;
const cipher = createCipheriv('aes-256-gcm', aesKey, iv);
cipher.setAAD(Buffer.from(aad, 'utf8'));
const ciphertext = Buffer.concat([cipher.update(rawBody, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
const envelope = JSON.stringify({ payload: ciphertext.toString('base64') });
const checksum = createHmac('sha256', checksumSecret)
.update(`${timestamp}.${clientId}.${envelope}`)
.digest('hex');
const res = await axios.post(`${gatewayBaseUrl}/process${path}`, envelope, {
headers: {
'Content-Type': 'application/json',
'X-Client-Id': clientId,
'X-Timestamp': String(timestamp),
'X-IV': iv.toString('base64'),
'X-Tag': tag.toString('base64'),
'X-Checksum': checksum,
'X-Request-Id': randomUUID(),
},
});
return res.data; // still encrypted — see Response decryption below
}
Response decryption
paymentSystem's reply comes back through the gateway encrypted the same way: { "payload": "..." } body, with X-IV / X-Tag / X-Timestamp / X-Checksum response headers (the response's own timestamp, not your request's — use it, not the one you sent, to build the AAD).
import { createDecipheriv } from 'crypto';
function decryptResponse(res, clientId, aesKey) {
const iv = res.headers['x-iv'];
const tag = res.headers['x-tag'];
const timestamp = res.headers['x-timestamp'];
if (!iv || !tag) return res.data; // unencrypted error object — see below
const aad = `${clientId}.${timestamp}`;
const decipher = createDecipheriv('aes-256-gcm', aesKey, Buffer.from(iv, 'base64'));
decipher.setAAD(Buffer.from(aad, 'utf8'));
decipher.setAuthTag(Buffer.from(tag, 'base64'));
const plaintext = Buffer.concat([
decipher.update(Buffer.from(res.data.payload, 'base64')),
decipher.final(),
]).toString('utf8');
return JSON.parse(plaintext);
}
If X-IV/X-Tag are absent, the response is an unencrypted error object (a request that never made it far enough to be processed and signed).
Replay protection & idempotency
X-Timestampoutside ±300 seconds of the gateway's clock →401.- Same
X-Request-Id+ identical body → the original response is replayed verbatim (X-Idempotent-Replay: trueheader), no re-execution — safe to retry a timed-out call with the same ID. - Same
X-Request-Id+ a different body →409(a bug, not a retry). - Same
X-Request-Idwhile the original call is still in flight →409.
Acting on behalf of a merchant
Every request body above must include merchantId for AEPS/DMT calls — see Vendor AEPS / Vendor DMT for why and where. UPI is a separate story — see Vendor UPI.
Logging in for account management
Your own account-management calls (profile, wallet, managed merchants, transactions, commission — see the overview) are a different, simpler flow: the same POST /login a merchant uses, just with your partner account's credentials, returning a JWT you send as Authorization: Bearer <token> — no encryption, no checksum, no gateway.
POST /login
Content-Type: application/json
{ "emailOrMobile": "partner@example.com", "password": "your-password" }
{
"statusCode": "00",
"message": "success",
"token": "eyJhbGciOiJIUzI1NiJ9...",
"user": { "name": "...", "role": "vendor" }
}
Same single-session behavior as the Merchant API's login — see Merchant API Authentication for the details, which apply identically here.
This JWT can also reach AEPS/DMT directly — but don't build on it
checkRole("merchant", "vendor") on every AEPS/DMT route means this same JWT, plus merchantId in the body, can call paymentSystem's AEPS/DMT routes directly with no gateway and no encryption at all. It works, but it isn't the credential model partner accounts are provisioned for — build against the API key + gateway flow on this page for anything you intend to run in production.