BucksBox Developer DocsBucksBox Developer Docs
Getting Started
Merchant API
Partner API
SDK
GitHub
Getting Started
Merchant API
Partner API
SDK
GitHub
  • Overview
  • Authentication
  • UPI
  • QR
  • AEPS
  • DMT

AEPS — Aadhaar Enabled Payment System

AEPS lets a merchant's agent perform Aadhaar/biometric-authenticated banking transactions — cash withdrawal, balance enquiry, mini statement, Aadhaar Pay — for a customer at the point of sale, without a card or PIN.

This page was rewritten against the live backend

The previous version of this page used operation names (aepsCashWithdrawal, aepsMerchantAuth, …) and fields (aadhaarNumber, bankIin, pidData) that do not match paymentSystem's actual AEPS module. What follows is read directly from src/modules/interface/nsdl/aeps/aeps.route.js, aeps.controller.js, aeps.txn.controller.js, and aeps.txn.service.js — including the sandbox fixtures in simulate.js, which is what you'll see running against a AEPS_SIMULATE=true deployment.

Two ways to call this

BaseAuthBody
Merchant — directpaymentSystem — POST /aeps/*Authorization: Bearer <JWT> (see Merchant API Authentication)Plain JSON, unencrypted
Vendor — via gatewayfintech-gateway — POST /process/aeps/*X-Client-Id + AES-256-GCM/HMAC envelope (see Partner API Security)Same plain JSON as the direct call, just encrypted — plus a merchantId field

fintech-gateway's /process/* route is a 1:1 path mirror, not an operation map — POST /process/aeps/withdrawal decrypts to and forwards exactly what POST /aeps/withdrawal would receive directly. The field names on this page apply to both paths identically; the only addition on the vendor side is merchantId in the body (see Access control below and Vendor AEPS).

/aeps/process is not live

An older gateway design (fintech-gateway/src/routes/aeps.js, an { "operation": "<name>", ...fields } envelope over POST /aeps/process) exists in the fintech-gateway codebase but is never registered in src/app.js — only the /process/* wildcard proxy above is actually mounted. If you've seen operation names or /aeps/process referenced elsewhere, that describes a design that isn't currently reachable.

Onboarding

One-time device/agent onboarding, in order. Every endpoint below is POST /aeps/<route> direct (merchant JWT) or POST /process/aeps/<route> via the gateway (vendor — add merchantId to the body; see Vendor AEPS). Request bodies below are the merchant/direct form; a vendor call is the same body plus merchantId.

Response shapes below

Where a response is paymentSystem's own construction (not NSDL passthrough), it's shown exactly as the controller builds it — verified against aeps.controller.js. Where the data payload is NSDL's own response forwarded as-is, that's called out explicitly; capture a real response in your environment rather than assuming the shown shape is complete.

initialize

POST /aeps/initialize
Authorization: Bearer <token>

No body. Creates the AEPS Service-KYC record for this merchant if one doesn't already exist.

{ "merchantId": "m-uuid", "service": "AEPS" }

Note the shape: this specific route does not wrap its response in the usual {statusCode, message, data} envelope — it returns merchantId and service directly at the top level on success (verified in aeps.controller.js#initialize). On failure it falls back to the standard envelope, e.g. { "statusCode": "500", "message": "Server error" }.

onboarding

POST /aeps/onboarding
Authorization: Bearer <token>

No body. Requires merchant.merchantinfo to already exist. Triggers NSDL onboarding and auto-requests the first OTP.

{
  "statusCode": "00",
  "otp_status": true,
  "otp_reqid": "OTPREQ-...",
  "...": "additional fields are NSDL's own onboarding response, passed through — not independently confirmed here"
}

Failure modes returned directly (not via NSDL): { "statusCode": "01", "message": "Merchant not found" } and { "statusCode": "01", "message": "Merchant info not created" }.

request_otp

POST /aeps/request_otp
Authorization: Bearer <token>

No body. Re-sends the onboarding OTP. Response is NSDL's request_otp reply, passed through — expect at least an otpreqid-shaped field on success; capture a real response to confirm the exact key names.

validate_otp

POST /aeps/validate_otp
Authorization: Bearer <token>
Content-Type: application/json

{ "otp": "123456", "otpreqid": "OTPREQ-..." }

Both fields required — otpreqid comes from onboarding/request_otp's response. Response is NSDL's validate_otp reply, passed through (statusCode: "00" on success is the one field this page confirms; the rest of data isn't independently captured here).

verify_pan

POST /aeps/verify_pan
Authorization: Bearer <token>
Content-Type: application/json

{ "pan": "ABCDE1234F", "name": "Ramesh Kumar", "dob": "1990-01-15" }

Response is NSDL's PAN-verification reply, passed through — not independently confirmed field-by-field here.

update_UID

POST /aeps/update_UID
Authorization: Bearer <token>
Content-Type: application/json

{ "aadhaarno": "999999999999" }

Response is NSDL's UID-update reply, passed through.

updatebiodetail

POST /aeps/updatebiodetail
Authorization: Bearer <token>
Content-Type: application/json

{ "PidData": "<PidData XML from the RD service>", "aadhaarno": "999999999999" }

Response is NSDL's biometric-update reply, passed through.

submit

POST /aeps/submit
Authorization: Bearer <token>

No body — uses the merchant's own appid as the NSDL reference. Finalizes onboarding.

{ "statusCode": "00", "message": "...", "data": { "...": "NSDL confirmation response, passed through" } }

statusCheck

GET /aeps/statusCheck
Authorization: Bearer <token>

No body. Response is NSDL's status-check reply for this merchant's onboarding, passed through.

GetAgentDetail

POST /aeps/GetAgentDetail
Authorization: Bearer <token>
Content-Type: application/json

{ "agentId": "" }

agentId optional — omit it (or send an empty string) and it defaults to your own terminal's mid. Response is NSDL's agent-detail reply, passed through.

Transactions

All five transaction operations share one request shape and one response envelope — differing only in which data fields NSDL actually populates. Every endpoint is POST /aeps/<route> direct (merchant JWT) or POST /process/aeps/<route> via the gateway (vendor — add merchantId).

FieldRequiredNotes
AadharNoYesCustomer's Aadhaar number
TxnAmountYes, alwaysRequired by the validator even for balance/minstatement/reqAuth — send 0
Latitude, LongitudeYesAgent's device location
CustomerNameYes
PidDataYesRaw PidData XML captured from a UIDAI-certified RD service app (in.gov.uidai.rdservice.fp.CAPTURE intent on Android — vendor-agnostic; Mantra, Morpho, Startek, SecuGen all implement the same intent contract)
BankNameNoThe receiving bank's IIN. Omit it and paymentSystem defaults to the sandbox test code 990326 — fine for AEPS_SIMULATE=true, not a real bank in production

Cash Deposit is not implemented yet

The checksum layer has a CD (Cash Deposit) transaction-type constant defined, but there's no /aeps/* route or controller function behind it — don't build against it as if it exists. If you need it, that's a paymentSystem change to request, not something to work around client-side.

withdrawal

POST /aeps/withdrawal
Authorization: Bearer <token>
Content-Type: application/json

{
  "AadharNo":     "999999999999",
  "TxnAmount":    500,
  "Latitude":     "19.0760",
  "Longitude":    "72.8777",
  "CustomerName": "Ramesh Kumar",
  "PidData":      "<PidData XML from the RD service>",
  "BankName":     "918152"
}
{
  "statusCode": "00",
  "message": "Transaction successful",
  "data": {
    "ResponseCode": "00",
    "ResponseStatus": "SUCCESS",
    "Balance": "15230.00",
    "STAN": "011032",
    "RetailerReferenceNumber": "RRN-...",
    "ReferenceNo": "...",
    "AuthCode": "cdcf3f268b87422f859e0b10676bfde8",
    "AadharNo": "********4821"
  }
}

Balance here is the balance after the withdrawal.

balance

POST /aeps/balance
Content-Type: application/json

{
  "AadharNo":     "999999999999",
  "TxnAmount":    0,
  "Latitude":     "19.0760",
  "Longitude":    "72.8777",
  "CustomerName": "Ramesh Kumar",
  "PidData":      "<PidData XML from the RD service>",
  "BankName":     "918152"
}
{
  "statusCode": "00",
  "message": "Transaction successful",
  "data": {
    "ResponseCode": "00",
    "ResponseStatus": "SUCCESS",
    "Balance": "15230.00",
    "STAN": "011033",
    "RetailerReferenceNumber": "RRN-...",
    "AadharNo": "********4821"
  }
}

minstatement

POST /aeps/minstatement
Content-Type: application/json

{
  "AadharNo":     "999999999999",
  "TxnAmount":    0,
  "Latitude":     "19.0760",
  "Longitude":    "72.8777",
  "CustomerName": "Ramesh Kumar",
  "PidData":      "<PidData XML from the RD service>",
  "BankName":     "918152"
}
{
  "statusCode": "00",
  "message": "Transaction successful",
  "data": {
    "ResponseCode": "00",
    "ResponseStatus": "SUCCESS",
    "Ministatement": "02/06 POS/D/5017000018 C 100.00\n01/06 POS/W/5017000017 D 500.00",
    "STAN": "011034",
    "AadharNo": "********4821"
  }
}

Ministatement is a single delimited string (one line per transaction, \n-separated), not a JSON array — NSDL's own format, passed through as-is.

reqAuth

POST /aeps/reqAuth
Content-Type: application/json

{
  "AadharNo":     "999999999999",
  "TxnAmount":    0,
  "Latitude":     "19.0760",
  "Longitude":    "72.8777",
  "CustomerName": "Ramesh Kumar",
  "PidData":      "<PidData XML from the RD service>",
  "BankName":     "918152"
}
{
  "statusCode": "00",
  "message": "Transaction successful",
  "data": {
    "ResponseCode": "00",
    "ResponseStatus": "SUCCESS",
    "STAN": "011030"
  }
}

A successful reqAuth caches the session in Redis for 24 hours — see Session reuse below. There's no Balance or Ministatement field on this one; it's purely an authentication handshake.

purchase

POST /aeps/purchase
Content-Type: application/json

{
  "AadharNo":     "999999999999",
  "TxnAmount":    350,
  "Latitude":     "19.0760",
  "Longitude":    "72.8777",
  "CustomerName": "Ramesh Kumar",
  "PidData":      "<PidData XML from the RD service>",
  "BankName":     "918152"
}
{
  "statusCode": "00",
  "message": "Transaction successful",
  "data": {
    "ResponseCode": "00",
    "ResponseStatus": "SUCCESS",
    "STAN": "011035",
    "RetailerReferenceNumber": "RRN-...",
    "ReferenceNo": "...",
    "AuthCode": "9f1c2a3b4d5e6f708192a3b4c5d6e7f8",
    "AadharNo": "********4821"
  }
}

Same response shape as withdrawal, minus Balance — this is a debit at the point of sale ("Aadhaar Pay"), not a cash disbursement, so there's no "balance after" to report.

Common response notes

  • ResponseCode "00" → success. Anything else → failed.
  • No response at all (NSDL network-level timeout) is a real, distinct outcome paymentSystem treats as pending, not failed — it still records the attempt with a generated reference (AEPS-NR-<timestamp>) so it can be reconciled later. Handle "no data" as pending in your own client too, rather than assuming a timeout means the money didn't move.

reqAuth and session reuse

NSDL requires a fresh authenticated session once per day per terminal. paymentSystem handles this for you automatically — if withdrawal/balance/ minStatement/purchase finds no active session in Redis for your terminal, it runs reqAuth inline before your transaction, then caches the session for 24 hours. Calling reqAuth explicitly first is optional, not required.

Access control

  • Every route above accepts both roles (checkRole("merchant", "vendor")) — a merchant's JWT or a vendor's gateway-authenticated call both work.
  • A vendor acting on behalf of a merchant must include merchantId in the body — paymentSystem resolves the merchant as req.user?.merchantId ?? req.body.merchantId. A merchant's JWT already carries merchantId, so the body field is what a vendor call uses instead. See the Vendor AEPS page for the vendor-specific shape.

RD Service Integration

PidData comes from a locally-running, UIDAI-certified RD service — the same standardized Intent contract on Android regardless of vendor:

ActionPurpose
in.gov.uidai.rdservice.fp.INFODetect the device, read DEVICE_INFO/RD_SERVICE_INFO extras
in.gov.uidai.rdservice.fp.CAPTURECapture a fingerprint; pass PID_OPTIONS XML in, read PID_DATA XML out

Never persist PidData or Aadhaar in full — capture, use once, discard. Mask Aadhaar in any UI/logs (paymentSystem itself already masks it in AadharNo on the way back, per simulate.js's maskAadhar()).

Supported Biometric (RD Service) Devices

Because the Intent contract above is UIDAI's own L1 spec, any UIDAI-certified RD service device should work — the platform doesn't hardcode a device allowlist. In practice, one vendor has been built against and verified end to end; the rest are named in the codebase but not yet confirmed against real hardware.

VendorStatusNotes
Mantra✅ VerifiedFingerprint (MFS100) confirmed working end to end, including through onboarding (Onboard.vue). RD service on 127.0.0.1:11100. Iris capture defaults to DeviceMake: "MANTRAIRIS" server-side (aeps.txn.service.js) when not overridden. The vendor's own SDK (Android_RD_SDK_Sample, Java, and Web packages) ships in this repo under MantraSDK/.
Morpho / Idemia⚠️ Listed, not verifiedReferenced in android-sdk's RD-vendor list and webpayment's device config as a placeholder port (11101) — explicitly commented "not verified against a live device."
Startek⚠️ Listed, not verifiedSame as above — placeholder port 11103, unverified.
SecuGen⚠️ Listed, not verifiedSame as above — placeholder port 11102, unverified.
Precision, Evolute⚠️ Named onlyAppear only in a source comment listing UIDAI-certified vendors — no port, no integration code, no testing evidence anywhere in the codebase.

What "verified" means here

"Verified" = there's working code and/or a confirmed local RD-service port exercised against real hardware somewhere in this repo. "Listed, not verified" = the vendor is named (usually as a config placeholder or a generic error-message suggestion like "install Mantra/Morpho/Startek/ SecuGen's RD service app") but nothing in the codebase has actually exercised that vendor's device. Since they all implement the same UIDAI Intent contract, they're expected to work — "not verified" is a testing gap here, not a known incompatibility. Confirm on real hardware before relying on a non-Mantra device in production.

Every RD service (any vendor) listens locally and exposes the same two actions over HTTP or Android Intent — INFO to detect the device and CAPTURE to run a capture and return PidData XML. Point your integration at whichever local port your installed RD service app uses; Mantra's default is 11100.

Transaction Flow

One-time setup (initialize → onboarding → request_otp → validate_otp → verify_pan → update_UID → updatebiodetail → submit) only runs again if you're re-onboarding the merchant — daily transacting only touches the right-hand branch above. balance/minstatement are typically called before withdrawal/purchase to confirm the account, but nothing enforces that ordering server-side.

Prev
QR
Next
DMT