KlyroPay API Documentation

Everything a reseller needs to integrate PayIn, Payout, DMT, AEPS & Merchant Onboarding

Base URL: https://admin.krestapay.com/api
KlyroPay Docs

Getting Started

All API requests require an API key in the request header.

Required Headers

X-Api-Key: your_api_key
Content-Type: application/json
IP Whitelist Required
Your server IP must be whitelisted before making API calls — this is mandatory, not optional. An account with no whitelist configured blocks every request. Submit your IP and callback URL via the dashboard. Admin approval required.

Authentication Flow

Request -> Check API Key -> Check Active/Expiry -> Check IP Whitelist -> Check Rate Limit -> Process

You also need: an active, KYC-verified account, a commission package that includes the service you're calling, and explicit service access granted to your account for each service. Any one of these missing will block the relevant calls even with a valid key.

What's Available

Service What it does Account model
PayIn Collect UPI payments from your customers Single wallet — your account only
Payout Send money to any bank account via IMPS Single wallet — your account only
DMT Domestic money transfer on behalf of a "remitter" (your end-customer) Single wallet — remitters/beneficiaries tracked by mobile number
Merchant Onboarding + AEPS Aadhaar-enabled cash withdrawal/balance/mini-statement at your retail outlets Multi-outlet — one API key, many outlet_ids (see below)
Two different account models on this platform:
  • PayIn / Payout / DMT — one API key = one wallet = your account. There's no sub-account concept; if you serve multiple downstream customers, tracking which transaction belongs to whom is your own responsibility (use order_id/partner_request_id).
  • AEPS / Merchant Onboarding — one API key can serve many outlets (your retailers). Each retailer onboards with their own Aadhaar/PAN, gets an outlet_id, and every AEPS call after that requires you to pass that specific outlet_id. See Merchant Onboarding for the full explanation.

PayIn Live

Collect UPI payments from your customers. Supports GPay, PhonePe, Paytm, BHIM and all UPI apps.

Create PayIn -> Get intent_url -> Show QR / Redirect customer -> Customer pays -> Webhook -> Wallet credited
Wallet Credit: Customer pays ₹1,000 -> Commission ₹10 -> ₹990 is credited to your unsettled_balance (not your spendable balance — see the Wallet section for why).
Expiry: Payment link expires in 10 minutes. Unpaid transactions are auto-marked as expired.
POST /api/v1/payin/create

Request Body

{
    "order_id":       "YOUR-ORDER-001",
    "amount":         1000,
    "customer_phone": "9876543210",
    "customer_name":  "Rahul Sharma"
}
Field Type Required Description
order_id string Optional Your unique order ID (max 100 chars, truncated to 35 chars when forwarded to the payment provider — keep it ≤35 chars). Strongly recommended: without it, retried/duplicate requests create separate PayIn links instead of being blocked.
amount number Required Collection amount in ₹ — minimum ₹300, maximum ₹1,00,000
customer_phone string Required Customer's 10-digit mobile number
customer_name string Optional Customer name (max 100 chars, truncated to 20 chars for the provider)

Response — 201

{
    "success": true,
    "data": {
        "transaction_id": "8a7f3e2d-9c1b-4f5a-b8e6-d4c2a1f9e7b3",
        "custom_txn_id":  "KRESTAA9MKMEQ5LMV29C2",
        "order_id":       "YOUR-ORDER-001",
        "txn_ref":        "PAYIN-REF-XXXXX",
        "amount":         1000.00,
        "commission":     10.00,
        "intent_url":     "upi://pay?pa=xxxxx&am=1000.00&cu=INR",
        "expires_in":     600,
        "status":         "pending"
    }
}
Careful with transaction_id: in this create response it's the internal ID. Everywhere else — status checks, transaction history, webhooks — the transaction_id field instead returns the human-readable custom_txn_id (e.g. KRESTAA9MKMEQ5LMV29C2). To avoid mismatches, always store and match on custom_txn_id — it's present in every response and never changes meaning. Use txn_ref (not transaction_id) for status checks below.
Field Description
custom_txn_id Your stable reference for this transaction — use this everywhere
txn_ref Save this — required for the status-check call below
intent_url UPI payment link — generate QR or redirect mobile users
expires_in Seconds until expiry (600 = 10 minutes)
commission Your service charge — deducted from credited amount

Showing the Payment to Customer

QR Code (Desktop)
<script src="https://cdnjs.cloudflare.com/ajax/libs/
qrcodejs/1.0.0/qrcode.min.js"></script>

<div id="qrcode"></div>
<input type="hidden" id="upi-url"
    value="{{ $intentUrl }}">

<script>
new QRCode(document.getElementById('qrcode'), {
    text: document.getElementById('upi-url').value,
    width: 220, height: 220
});
</script>
UPI Button (Mobile)
<!-- Pass via hidden input to avoid
     HTML encoding issues -->
<input type="hidden" id="upi-url"
    value="{{ $intentUrl }}">

<button onclick="
    window.location.href =
    document.getElementById('upi-url').value
">
    Pay with UPI App
</button>
GET /api/v1/payin/status/{txn_ref}

If the transaction is already in a terminal state (success/failed/expired), this returns the stored record instantly. Otherwise it actively re-checks with the payment provider first — safe to use as a polling fallback if you haven't set up webhooks yet.

{
    "success": true,
    "data": {
        "transaction_id":  "KRESTAA9MKMEQ5LMV29C2",
        "internal_txn_id": "8a7f3e2d-9c1b-4f5a-b8e6-d4c2a1f9e7b3",
        "custom_txn_id":   "KRESTAA9MKMEQ5LMV29C2",
        "order_id":        "YOUR-ORDER-001",
        "txn_ref":         "PAYIN-REF-XXXXX",
        "amount":          1000.00,
        "commission":      10.00,
        "utr":             "407241812279",
        "status":          "success",
        "created_at":      "2026-04-30T10:00:00.000000Z"
    }
}
Status Meaning
pending Awaiting customer payment
success Payment received — credited to your unsettled_balance
failed Payment failed
expired 10 minutes passed without payment
GET /api/v1/payin/transactions
Param Description Default
page Page number 1
limit Records per page (max 100) 15

Payout Live

Transfer money directly to any bank account via IMPS.

Create Payout -> Wallet debited (amount + commission) -> Bank transfer -> Webhook on final status
Wallet Debit: Transfer ₹5,000 + Commission ₹10 = ₹5,010 debited from available_balance
Refund: If a transfer doesn't go through, the full amount is automatically refunded — whether it comes back to you tagged refunded or failed, treat both the same way: money back in your wallet, transfer did not happen.
POST /api/v1/payout/process

Request Body

{
    "mobile_no":          "9876543210",
    "beneficiary_name":   "Rahul Sharma",
    "account_no":         "1234567890",
    "ifsc":               "SBIN0001234",
    "bank_name":          "State Bank of India",
    "amount":             5000,
    "partner_request_id": "YOUR-UNIQUE-ORDER-ID"
}

Note: transfers are always IMPS — there's no transfer_type field to send; it's handled automatically on our side.

Field Type Required Description
mobile_no string Required 10-digit mobile number
beneficiary_name string Required Account holder name (max 100 chars)
account_no string Required Bank account number (8–20 characters)
ifsc string Required IFSC code — format: ABCD0123456. Must be sent in UPPERCASE, e.g. sbin0001234 fails validation even though SBIN0001234 passes
bank_name string Required Bank name (max 100 chars)
amount number Required Transfer amount in ₹ (min ₹10 — max ₹2,00,000)
partner_request_id string Optional Your unique order ID. Strongly recommended for duplicate prevention — without it, a retried request can trigger a second real transfer.

Response — Success

{
    "success": true,
    "message": "Payout transferred successfully.",
    "data": {
        "transaction": {
            "transaction_id":     "KRESTAA9MKMEQ5LMV29C2",
            "internal_txn_id":    "8e6bf7e9-8e19-47f3-affc-88d02f69b86a",
            "custom_txn_id":      "KRESTAA9MKMEQ5LMV29C2",
            "reference_id":       "RKIT-ORDER-OR-UTR-XXXX",
            "partner_request_id": "YOUR-UNIQUE-ORDER-ID",
            "status":             "success",
            "amount":             5000.00,
            "charge":             10.00,
            "net_amount":         5010.00
        }
    }
}

Note: the commission field here is called charge, not commission — a naming quirk specific to this endpoint. Webhooks (below) use commission instead.

Response — Pending

{
    "success": true,
    "message": "Payout is being processed. You will be notified once settled.",
    "data": {
        "transaction": {
            "transaction_id":     "KRESTAA9MKMEQ5LMV29C2",
            "internal_txn_id":    "8e6bf7e9-8e19-47f3-affc-88d02f69b86a",
            "custom_txn_id":      "KRESTAA9MKMEQ5LMV29C2",
            "reference_id":       null,
            "partner_request_id": "YOUR-UNIQUE-ORDER-ID",
            "status":             "pending",
            "amount":             5000.00,
            "charge":             10.00,
            "net_amount":         5010.00
        }
    }
}
Note: UTR number is NOT available in the initial API response. It will be sent via callback webhook once the bank processes the transfer (typically within 1-2 minutes). Also note: if the request fails at the network/server level (rather than being cleanly accepted or rejected by the bank), the response won't include a transaction block at all — just an error message. The refund still happens automatically; check the webhook or the status endpoint below to see it reflected.
Status Meaning
success Money credited to beneficiary
pending Bank processing — callback will follow
failed Transfer failed — amount already refunded to wallet
refunded Transfer failed — amount refunded to wallet
GET /api/v1/transactions/service/payout
Param Description Default
status Filter: success / pending / failed / refunded All
from_date Start date — YYYY-MM-DD
to_date End date — YYYY-MM-DD
page Page number 1
limit Records per page (max 100) 15

Single Transaction

GET /api/v1/transactions/service/payout/{transaction_id}

Accepts either your custom_txn_id (KRESTA...) or the internal UUID — both work. There's no dedicated /payout/status/{id} route; this shared endpoint is the way to look up a payout after creation.

DMT — Domestic Money Transfer Requires RD Device

IMPS/NEFT remittance on behalf of a "remitter" (your end-customer) to any bank beneficiary — a strictly sequential, 8-step regulatory flow.

Why so many steps? Each step is an RBI/UIDAI-mandated regulatory checkpoint (identity verification -> biometric confirmation -> beneficiary verification -> fresh OTP per transaction -> transfer) — not arbitrary complexity. Steps 2–4 are needed only for brand-new remitters; once a remitter is registered and KYC-verified, new transfers only need steps 5–8 (or 7–8 for an already-verified beneficiary).
Never re-run Steps 2/3/4 on an existing remitter. Check exists/is_kyc_verified from Step 1 first — if is_kyc_verified: true, skip straight to Step 5 or Step 7. Re-running Step 2 on an already-registered remitter is now blocked with a clear 422 before it ever reaches the provider, but avoid the wasted round-trip by checking client-side too.
1 POST /api/v1/dmt/remitter/profile

Call this first, every time. Tells you if the mobile number is a new or existing remitter, and if existing, returns their beneficiaries + KYC status in one call.

Field Type Required Description
mobile_number string Required 10-digit remitter mobile number

Response — New Mobile Number

{
  "success": true,
  "message": "Remitter Not Found",
  "data": {
    "exists": false,
    "is_verified": false,
    "is_kyc_verified": false,
    "statuscode": "RNF",
    "status": "Remitter Not Found",
    "data": {
      "referenceKey": "mTN2LUVBFvr034...",
      "pidOptionWadh": "E0jzJ/P8UopUHAieZn8CKqS4WPMi5ZSYXgfnlfkWjrc="
    }
  }
}

Response — Already Exists & KYC-Verified

{
  "success": true,
  "message": "Success",
  "data": {
    "exists": true,
    "is_verified": true,
    "is_kyc_verified": true,
    "statuscode": "TXN",
    "status": "Success",
    "data": {
      "mobileNumber": "9998887776",
      "firstName": "Ramesh",
      "lastName": "Kumar",
      "limitAvailable": "24143.00",
      "beneficiaries": [
        {
          "id": "2cbd9684548d5b36335876735d8cee90",
          "name": "Suresh Verma",
          "account": "50100123456789",
          "ifsc": "HDFC0000123",
          "bank": "HDFC BANK",
          "beneficiary_id": "7c1e2f3a-1234-4abc-9def-1234567890ab"
        }
      ],
      "referenceKey": "o6p8y4S/xHSwmh53...",
      "pidOptionWadh": "E0jzJ/P8UopUHAieZn8CKqS4WPMi5ZSYXgfnlfkWjrc="
    }
  }
}
Field Description
exists Our own field — whether this mobile number is already a remitter (corrected for a provider quirk, see below)
is_verified Registration (Step 2) + OTP (Step 3) done or not
is_kyc_verified Biometric eKYC (Step 4) done or not — Step 5 depends on this being true
data.beneficiaries[].beneficiary_id Our internal UUID, injected when a match is found — use this (not the provider's own id) in every other DMT call
data.pidOptionWadh Needed for Step 4's biometric capture — fresh every ~15 min, always re-fetch right before capturing, never cache
Provider quirk: the provider itself reports "Remitter Not Found" until eKYC is complete — even after Register + Verify OTP. We cross-check our own local record so exists/is_verified/is_kyc_verified are always accurate, even mid-onboarding. Decide your next step from these three flags: is_verified: false -> Step 2. is_verified: true, is_kyc_verified: false -> Step 4. is_kyc_verified: true -> Step 5 or 7.
2 POST /api/v1/dmt/remitter/register

Only when Step 1 returned exists: false. Links the remitter's Aadhaar with the provider and sends an OTP.

Field Type Required Description
mobile_number string Required 10-digit remitter mobile number
aadhaar_number string Required 12-digit Aadhaar (AES-encrypted server-side before upstream — never logged/stored plaintext)

Response — OTP Sent

{
  "success": true,
  "message": "OTP Successfully sent",
  "data": {
    "statuscode": "OTP",
    "status": "OTP Successfully sent",
    "data": { "validity": "2026-08-04 00:24:27", "referenceKey": "W69kpF1qSYuLOenm..." }
  }
}

OTP goes to the remitter's mobile — use it in Step 3 immediately, validity is ~15 min.

Response — Already Registered (called by mistake)

{ "success": false, "message": "This customer is already registered — there is no need to register them again. You can continue directly to adding their beneficiary and sending money.", "data": null }

HTTP 422 — blocked before reaching the provider.

3 POST /api/v1/dmt/remitter/verify-otp
Field Type Required Description
mobile_number string Required Same as Step 2
otp string Required 6-digit OTP received by the remitter

Response — Success

{
  "success": true,
  "message": "Mobile validated successfully please proceed for kyc",
  "data": { "success": true, "statuscode": "KYC", "data": { "referenceKey": "74kzm0Db..." } }
}
Invalid/expired referenceKey means too much time passed between Step 2 and 3 — re-run Step 2 fresh and complete Step 3 immediately with the new OTP. This failed attempt is still chargeable.
4 POST /api/v1/dmt/remitter/kyc

Requires a real biometric capture from a UIDAI-certified RD Service device (e.g. Mantra MFS110) or AadhaarFaceRD — this data cannot be fabricated, it must come from actual certified capture hardware/software on your end.

Field Type Required Description
mobile_number string Required Verified remitter's mobile number
capture_type string Optional FINGER (default) or FACE
biometric_data object Required Raw RD Service PID block, unmodified — see below
latitude/longitude number Optional Capture location

biometric_data shape

{
  "ci": "20280813", "hmac": "<from RD Service>", "pidData": "<base64>",
  "ts": "2026-08-04T11:09:59", "dc": "00c7f4af-...", "mi": "MFS110",
  "dpId": "MANTRA.MSIPL", "mc": "<from RD Service>",
  "rdsId": "RENESAS.MANTRA.001", "rdsVer": "1.5.1",
  "Skey": "<from RD Service>", "srno": "9604562"
}
Chargeable even on failure. A mismatch/incomplete capture ("Missing biometric data...") still costs money per attempt — validate your device's capture (non-empty pidData/hmac/Skey, errCode: "0") before submitting.

Success unlocks Step 5. This is only needed for brand-new remitters — check is_kyc_verified from Step 1 first.

GET /api/v1/dmt/banks/search?query=...

Needed before Step 5 — resolves a numeric bank_id from an IFSC code or bank name.

Param Required Description
query Required Full/partial IFSC (e.g. HDFC0000123) or a bank-name substring (e.g. state bank)
force_refresh Optional true to bypass the 24h cache
{
  "success": true, "message": "OK",
  "data": {
    "query": "HDFC0000123",
    "matches": [ { "bank_id": 11263, "name": "HDFC BANK", "ifsc_alias": "HDFC", "imps": true, "neft": true } ],
    "total_banks": 1847
  }
}

Cached 24h per provider — repeated calls don't cost anything extra.

5 POST /api/v1/dmt/beneficiary/add

Only after is_kyc_verified: true. Registers who the money is going to.

Field Type Required Description
remitter_mobile_number string Required Verified & KYC-completed remitter
beneficiary_mobile_number string Required Beneficiary's mobile number
account_number string Required 8–20 characters
ifsc string Required Format AAAA0######
bank_id integer Required From Bank ID Lookup above
name string Required Beneficiary account holder name, max 100 chars

Response — OTP Sent

{
  "success": true, "message": "OTP Successfully sent",
  "data": {
    "beneficiary_id": "7c1e2f3a-1234-4abc-9def-1234567890ab",
    "statuscode": "OTP", "status": "OTP Successfully sent",
    "data": { "beneficiaryId": "2cbd9684...", "referenceKey": "gJFqtmhE..." }
  }
}
beneficiary_id is our internal UUID — use it in every subsequent step (6/7/8/Delete). data.beneficiaryId is the provider's own ID — never pass that one back to us.
6 POST /api/v1/dmt/beneficiary/verify-otp

The OTP from Step 5 goes to the remitter's mobile (not the beneficiary's) — the remitter is confirming they want to add this beneficiary.

Field Type Required Description
beneficiary_id string (uuid) Required From Step 5's response
otp string Required 6-digit OTP received by the remitter
{ "success": true, "message": "Success", "data": { "success": true, "beneficiary_id": "7c1e2f3a-...", "statuscode": "TXN" } }
Delete Beneficiary (2-step: Initiate -> Confirm — not part of the sequential flow)

Any already-verified beneficiary can be deleted at any time.

POST /api/v1/dmt/beneficiary/delete

Sends an OTP to the remitter's mobile — does not delete by itself. Body: beneficiary_id (must have no pending transfer, or you get 409).

POST /api/v1/dmt/beneficiary/delete-verify

Body: beneficiary_id, otp. On success the beneficiary is permanently removed and its ID becomes invalid everywhere else.

Response has two success fields — outer (request processed) and data.success (deletion actually happened, e.g. false on a wrong OTP). Check both.
7 POST /api/v1/dmt/transfer/generate-otp

Required immediately before every transfer — Step 6's OTP cannot be reused.

Field Type Required Description
beneficiary_id string (uuid) Required Must already be OTP-verified (Step 6)
amount number Required Intended transfer amount (min 1)
{ "success": true, "message": "OTP Successfully sent", "data": { "beneficiary_id": "7c1e2f3a-...", "statuscode": "OTP", "data": { "referenceKey": "NjINNkM5...", "isTxnBioAuthRequired": false } } }
8 POST /api/v1/dmt/transfer
Field Type Required Description
beneficiary_id string (uuid) Required Same as Step 7
transfer_mode string Required IMPS or NEFT
amount number Required Must match Step 7 — min ₹1, max ₹2,00,000 (also bound by your package's slab range)
otp string Required 6-digit OTP from Step 7
partner_request_id string Optional Your idempotency key (max 100 chars) — safely retry a timed-out call with the same value and get 409 instead of a double-send

Response — Success

{
  "success": true, "message": "DMT transfer completed successfully.",
  "data": {
    "transaction": {
      "transaction_id": "TXN20260802ABCDE", "internal_txn_id": "b3f1c2d4-...",
      "reference_id": "IP20260802123456", "utr": "IP20260802123456",
      "partner_request_id": "RESELLER-ORDER-9981", "status": "success",
      "amount": 5000, "commission": 12.5, "net_amount": 5012.5
    },
    "statuscode": "TXN", "status": "Success", "data": { "txnReferenceId": "IP20260802123456" }
  }
}

Response — Pending (timeout, no definitive answer yet)

{
  "success": true, "message": "DMT transfer initiated. Check status shortly.",
  "data": {
    "transaction": { "transaction_id": "TXN20260802ABCDE", "status": "pending", "amount": 5000, "commission": 12.5, "net_amount": 5012.5 },
    "error": "cURL error 28: Operation timed out"
  }
}
A timeout/ambiguous response is never treated as "failed." It stays pending until a definitive failure is confirmed (via Status Check) — this avoids a "double-loss" where money actually transferred but got refunded anyway. Follow the same rule in your own integration.

Response — Failed & Refunded

{ "success": false, "message": "DMT transfer failed. Amount has been refunded to your wallet.", "data": { "transaction": { "status": "refunded", "amount": 5000, "commission": 12.5, "net_amount": 5012.5 } } }
GET /api/v1/dmt/status/{transactionId}

{transactionId} is the transaction_id from Step 8. Resolved transactions return instantly from our own records; only still-pending ones trigger a fresh provider check internally.

{
  "success": true, "message": "DMT status retrieved.",
  "data": {
    "transaction_id": "TXN20260802ABCDE", "utr": "IP20260802123456",
    "partner_request_id": "RESELLER-ORDER-9981", "amount": 5000,
    "commission": 12.5, "net_amount": 5012.5, "status": "success",
    "created_at": "2026-08-02T16:33:59.000000Z"
  }
}

Common Pitfalls

Pitfall Fix
Re-running Register/Verify/eKYC on an existing remitter Check is_kyc_verified from Step 1 first — skip straight to Step 5/7 if true
OTP session expired ("Invalid referenceKey") Sessions last ~15 min — complete each OTP chain immediately, don't delay between steps
Reusing a cached pidOptionWadh Always re-fetch via Step 1 right before a biometric capture — it expires in ~15 min too
Treating a transfer timeout as "failed" Treat as pending, confirm via Status Check before assuming failure
Retrying /transfer after a timeout without partner_request_id Always send one — lets a retry return 409 instead of risking a second real transfer

Error Reference

Code Message Reason
404 Beneficiary not found on your account Wrong/deleted beneficiary_id
409 A transfer to this beneficiary is already pending Check status before retrying
409 Duplicate request — partner_request_id already exists Safe-retry protection — check status of the original instead
409 Cannot delete a beneficiary with a transfer still in progress Resolve the pending transfer first
422 Amount ₹X is outside the allowed range Outside your package's slab range for DMT

Merchant Onboarding Live

The prerequisite for AEPS — every retailer/outlet must be onboarded here before any AEPS transaction will work for them.

Multi-Retailer Architecture

This is the most important concept to understand before integrating AEPS. Your platform uses one API key — but each retailer onboards with their own Aadhaar/PAN and gets a unique outlet_id. You save that outlet_id against your retailer's record, and pass it explicitly on every AEPS call for that retailer.

Your SaaS Platform (1 API key) | |-- Retailer A -- own Aadhaar/PAN -- outlet_id: "714724" |-- Retailer B -- own Aadhaar/PAN -- outlet_id: "719981" \-- Retailer C -- own Aadhaar/PAN -- outlet_id: "725310" Every AEPS call: {"outlet_id": "<that retailer's>", ...}
Security: an outlet_id only works with the API key that onboarded it — a different reseller's key gets 404 Outlet not found on your account. Within your own key, though, sending the right outlet_id for the right retailer is your responsibility — we can't tell which of your retailers a call is "really" for.
1 POST /api/v1/merchant-onboarding/signup

Onboard a new retailer, once. Needs the retailer's own identity (Aadhaar/PAN/name/DOB) — the outlet is registered under their own identity.

Field Required Description
mobile Yes 10-digit, Aadhaar-registered mobile
name Yes Must exactly match the PAN name
gender Yes M, F, or T
pan Yes 10-character individual PAN
email Yes Valid email
address_full, address_city, address_pincode Yes Must match the Aadhaar-registered address
aadhaar_number Yes 12-digit — encrypted server-side, send plaintext to us
date_of_birth Yes YYYY-MM-DD
latitude/longitude Yes Real outlet/shop location — used for geo-fencing on every future transaction

Response

{
  "success": true, "message": "Transaction Successful",
  "data": {
    "success": true, "outlet_id": "714724",
    "raw": { "data": { "outletId": 714724, "name": "A**HIT G**TA", "state": "RAJASTHAN" } }
  }
}
Save data.outlet_id immediately against the retailer's record. This response is the only place you get it — signing up again doesn't mint a new one, it just updates the existing outlet per the provider's docs.
2 POST /api/v1/merchant-onboarding/biometric-kyc-status

Call right after Signup to check whether biometric submission is needed.

Field Required Description
outlet_id Yes From Signup
sp_key Yes Always send "WAP" for AEPS
{
  "success": true, "data": {
    "action_required": true, "status": "PENDING",
    "outlet_aadhaar": null,
    "reference_key": "GioMS1k4VyBbY/r3LUzyR9sSaXtEIb6R3nAjtq9gL9AvqMO/4A0wgRZAarU7y7Sn"
  }
}
Field Description
action_required true -> Biometric KYC (Step 3) is required next
outlet_aadhaar Non-null -> provider already has an Aadhaar on file, skip sending it in Step 3
reference_key Required by Step 3 — single-use
3 POST /api/v1/merchant-onboarding/biometric-kyc

Only when Step 2 returned action_required: true. Requires the retailer's own fingerprint capture.

Field Required Description
outlet_id Yes
reference_key Yes From Step 2
aadhaar_number No Only if Step 2's outlet_aadhaar was empty
capture_type No FINGER or FACE, default FINGER
biometric_data Yes RD Service capture output — same shape as DMT's, see above
{ "success": true, "data": { "success": true, "message": "Biometric KYC completed" } }
Success here means submission was accepted — it doesn't mean bank approval. Use Step 4 (AEPS Readiness) to confirm you're actually ready to transact.
4 POST /api/v1/merchant-onboarding/aeps-readiness

Call after Step 3, then periodically until aeps_ready: true. This is the real signal that the outlet can transact — bank approval takes time.

Field Required
outlet_id Yes
{
  "success": true, "data": {
    "aeps_ready": true, "wap_status": true,
    "biometric": { "status": "Biometric authentication already completed", "action_required": false },
    "record": { "outletId": 714724, "products": { "wapStatus": true } }
  }
}

aeps_ready: false -> any AEPS call returns 403. Poll this until true. aeps_ready/wap_status reflect bank approval; biometric.* reflects biometric-KYC completion — two separate checks bundled into one call.

AEPS Requires RD Device

Aadhaar Enabled Payment System — cash withdrawal, balance enquiry, and mini-statement at an onboarded outlet. Requires Merchant Onboarding to be complete for that outlet first.

Key Rules

Rule Why
One outlet_id = one retailer = one physical shop, always UIDAI compliance — daily biometric 2FA, geo-fencing, and bank approval are all per-outlet
Geo-fencing: transaction location must be within 3km of the outlet's registered location Provider's AEPS compliance requirement
2 failed Cash Withdrawals on the same customer Aadhaar -> auto-blocked Fraud prevention — customer-level, not retailer-level. Needs admin EDD to unblock
Outlet Login (daily 2FA) uses the retailer's Aadhaar/finger — everything else uses the customer's Two different people, don't mix them up
POST /api/v1/aeps/banks

Call before any transaction to find the customer's bank — gives you the bankiin every other AEPS call needs.

Field Required Description
outlet_id Yes
force_refresh No Default cached response (24h)
Response is the raw provider payload, not wrapped in the standard data envelope.
{
  "statuscode": "TXN", "status": "Banks fetched successfully",
  "data": [
    { "bankId": 109005, "name": "STATE BANK OF INDIA", "iin": "607094", "aepsEnabled": true },
    { "bankId": 1, "name": "AIRTEL PAYMENTS BANK", "iin": "990320", "aepsEnabled": true }
  ]
}

iin is what you pass as bankiin in every other AEPS call.

POST /api/v1/aeps/outlet-login-status

Check daily, before any transaction — has today's 2FA already been done?

Field Required
outlet_id Yes
type No — leave blank except for "CashDeposit"
{ "success": true, "data": { "logged_in": false, "actcode": "LOGINREQUIRED" } }

logged_in: false -> run Outlet Login first.

POST /api/v1/aeps/outlet-login

Once per calendar day (expires at midnight), before any transaction. Uses the retailer's own Aadhaar and fingerprint — not the customer's.

Field Required Description
outlet_id Yes
aadhaar_number Yes Retailer/outlet-owner's own Aadhaar
biometric_data Yes Retailer's own fingerprint capture
latitude/longitude Yes Transaction-time location — checked for geo-fencing
{ "success": true, "data": { "success": true, "message": "SUCCESS" } }
Chargeable on every attempt, success or fail. If you see "Biometrics locked by Aadhaar holder", the retailer has locked their own biometric via the UIDAI app/portal — unlocking is their responsibility.
POST /api/v1/aeps/transaction-otp

Only needed when the Cash Withdrawal amount will be above ₹5,000.

Field Required
outlet_id, bankiin Yes
aadhaar_number, mobile (customer's) Yes
amount, latitude/longitude Yes
{ "success": true, "data": { "message": "OTP Successfully sent", "reference_key": "F7Z7WzbRJZeF5nM1...", "otp_required": true } }

otp_required: true (off-us) -> embed the customer's OTP in the withdrawal capture. false (on-us) -> no OTP needed, but reference_key is still required in the withdrawal call.

POST /api/v1/aeps/cash-withdrawal
Field Required Description
outlet_id, bankiin Yes
mobile, aadhaar_number Yes Customer's
amount Yes Max ₹50,000
biometric_data Yes Customer's fingerprint capture
reference_key Conditional Required above ₹5,000 — from Transaction OTP
latitude/longitude Yes Checked for geo-fencing — missing/wrong coordinates directly cause rejection, there's no fallback to the outlet's registered location

Response — Success

{
  "success": true, "data": {
    "success": true, "message": "Transaction Successful",
    "transaction": { "transaction_id": "KRESTAA9MKMEQ5LMV29C2", "status": "success", "amount": 500, "commission": 5.0 },
    "provider": { "bankName": "State Bank of India", "bankAccountBalance": "1520.00" }
  }
}
The outer success: true only means "request processed" — check data.success/data.transaction.status for the real outcome. A rejected withdrawal is still HTTP 200.

Common bank/UIDAI-side rejection messages: "No account found for given Aadhaar number", "Pre auth failed for given request", "U16Risk threshold has exceeded", "UIDAI_ERROR552 Invalid wadh element".

POST /api/v1/aeps/balance-enquiry

Same body as Cash Withdrawal, minus amount/reference_key.

{ "success": true, "data": { "success": true, "balance": "1520.00" } }
POST /api/v1/aeps/mini-statement

Same body as Balance Enquiry.

{
  "success": true, "data": {
    "success": true,
    "mini_statement": [
      { "date": "02/08/2026", "txnType": "CR", "amount": "1.00", "narration": "01015522" },
      { "date": "18/07/2026", "txnType": "DR", "amount": "2000.00", "narration": "NFI/CASH" }
    ]
  }
}

Error Reference

Code Message Reason
403 This outlet is not yet approved for AEPS by the bank Check AEPS Readiness before retrying
403 This Aadhaar is blocked after repeated biometric mismatches Needs EDD (physical re-verification) via admin
404 Outlet not found on your account Missing/wrong outlet_id, or it belongs to a different reseller
422 Transaction location is Xkm from the registered outlet Geo-fence violation — max 3km
422 reference_key is required for withdrawals above ₹5,000 Run Transaction OTP first

Wallet

Check your wallet balance and transaction history.

GET /api/v1/wallet/balance
{
    "success": true,
    "data": {
        "wallet_id":         "9c1b4f5a-...",
        "total_balance":     "46000.0000",
        "available_balance": "45000.0000",
        "hold_balance":      "0.0000",
        "unsettled_balance": "1000.0000"
    }
}
Two balances, not one. This is the single most important thing to understand about your wallet:
  • available_balance — your spendable balance. Payouts/DMT debit from here, and refunds land back here.
  • unsettled_balance — money your customers have paid you via PayIn. It sits here until klyropay runs settlement (a manual/batch step on our side, not instant) and moves it into available_balance.
In short: a successful PayIn does not immediately give you money you can spend on a Payout — it has to be settled first.
Field Description
available_balance Spendable now — usable for Payout/DMT
unsettled_balance PayIn collections not yet settled to available_balance
hold_balance Amount currently locked/on-hold — not available for use
total_balance Sum of all three above — informational only, not a single spendable number
GET /api/v1/wallet/transactions

Full ledger of every debit/credit — useful for reconciling refunds and settlements against your own records.

Callback (Webhook)

klyropay sends POST requests to your registered callback URL when a transaction status changes — currently wired for PayIn and Payout. (DMT and AEPS resolve synchronously in the API response, or via status-polling — see their sections above.)

Payout callbacks — 1 or 2, depending on how the bank responds:
If the provider settles asynchronously, you'll get 2 callbacks — first pending (immediately, utr not yet known), then the final success/failed/refunded once the bank confirms. If the provider resolves the transfer instantly (accepted or rejected on the spot), you'll get just 1 callback already carrying the final status.

PayIn: Single callback on final status (success, failed, expired)
Retry: up to 5 attempts, 5 minutes apart — but only if delivery itself fails (timeout, connection error). If your server responds with a non-200 HTTP status (4xx/5xx) but the request otherwise completes, it is not automatically retried — it's logged as undelivered. Always poll the relevant status endpoint as a fallback rather than relying solely on retries.

Callback Request Headers

POST https://yourserver.com/your-callback-endpoint
Content-Type: application/json
X-Krestapay-Event: transaction.updated
X-Krestapay-Signature: hmac_sha256_signature

Note: the header prefix is X-Krestapay-* (klyropay's underlying platform name) — not X-KlyroPay-*. Match on the exact header name shown above.

PayIn Callback Payload

{
    "event":              "transaction.updated",
    "transaction_id":     "KRESTAA9MKMEQ5LMV29C2",
    "partner_request_id": null,
    "service":            "payin",
    "status":             "success",
    "amount":             300.00,
    "commission":         3.00,
    "net_amount":         297.00,
    "txn_ref":            "ORD1778403905ZNCI",
    "utr":                "60750480359",
    "order_id":           "ORD-260510143504-ZNCI",
    "timestamp":          "2026-05-10T14:35:16.000000Z"
}

partner_request_id is always null for PayIn — use order_id (your own reference) or transaction_id to correlate instead.

Payout Callback #1 — Pending

{
    "event":              "transaction.updated",
    "transaction_id":     "KRESTA0D7PUDJTSHLYX",
    "partner_request_id": "APPV26051011205050185762",
    "service":            "payout",
    "status":             "pending",
    "amount":             1080.00,
    "commission":         21.60,
    "net_amount":         1101.60,
    "utr":                null,
    "timestamp":          "2026-05-10T05:50:53.398478Z"
}

Payout Callback #2 — Success

{
    "event":              "transaction.updated",
    "transaction_id":     "KRESTA0D7PUDJTSHLYX",
    "partner_request_id": "APPV26051011205050185762",
    "service":            "payout",
    "status":             "success",
    "amount":             1080.00,
    "commission":         21.60,
    "net_amount":         1101.60,
    "utr":                "613011436999",
    "timestamp":          "2026-05-10T05:52:03.203539Z"
}

Payout Callback #2 — Failed

{
    "event":              "transaction.updated",
    "transaction_id":     "KRESTA0D7PUDJTSHLYX",
    "partner_request_id": "APPV26051011205050185762",
    "service":            "payout",
    "status":             "refunded",
    "amount":             1080.00,
    "commission":         21.60,
    "net_amount":         1101.60,
    "utr":                null,
    "timestamp":          "2026-05-10T05:51:15.123456Z"
}

Callback Flow

Payout Initiated -> Callback #1 (pending, utr=null) -> Bank Processing -> Callback #2 (success, utr=613011436999)
Field Description
transaction_id Your custom_txn_id — never the internal UUID
partner_request_id Your order ID for Payout (always null for PayIn — use order_id instead)
status pending -> success / failed / refunded
utr Bank UTR number — null until settlement completes, populated in the final callback
timestamp Callback sent time (ISO 8601)

Verify Signature

Always verify the signature to ensure the callback is genuinely from klyropay.

The signing secret is NOT your X-Api-Key. It's a separate secret issued once when your API key was created (shown to you a single time — store it securely, it can't be re-displayed later). Using your X-Api-Key value here will make every signature check fail.
PHP
$sig      = $_SERVER['HTTP_X_KRESTAPAY_SIGNATURE'];
$rawBody  = file_get_contents('php://input');
$payload  = json_decode($rawBody, true);
$expected = hash_hmac(
    'sha256',
    $rawBody,               // hash the raw bytes, not a re-encoded copy
    'YOUR_SIGNING_SECRET'   // issued at key-creation time — not your API key
);

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}

// Valid — process the callback
http_response_code(200);
echo json_encode(['status' => 'ok']);
Node.js
const crypto = require('crypto');

const sig = req.headers[
    'x-krestapay-signature'
];
// Hash the raw request body bytes, not
// JSON.stringify(req.body) — re-serializing
// can change key order/spacing and break
// the comparison.
const expected = crypto
    .createHmac('sha256', 'YOUR_SIGNING_SECRET')
    .update(req.rawBody)
    .digest('hex');

if (!crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(sig)
)) {
    return res.status(401).send('Unauthorized');
}

res.status(200).json({ status: 'ok' });

Error Reference

All error responses follow this format:

{ "success": false, "message": "Error description.", "data": null }

Common — All Services

HTTP Code Message Reason
401 API key is missing X-Api-Key header not sent
401 Invalid API key Key is incorrect
401 API key is inactive or expired Key revoked or expired
403 No IP whitelist configured No IP approved yet
403 Your IP is not whitelisted Request from non-whitelisted IP
403 You do not have access to this service Service not assigned to your account specifically
403 No package assigned to your account Ask support to assign a commission package
403 Your package does not include this service Your package has no commission slab for this service
409 Duplicate request Same idempotency key (partner_request_id/order_id) already in use
422 Validation failed Invalid request fields
422 Insufficient wallet balance Not enough available_balance
422 Minimum/Maximum amount is ₹X Amount outside the provider's allowed range
422 Amount ₹X is outside the allowed range Amount outside your package's commission slab range (checked separately from the above)
429 Rate limit exceeded Too many requests per minute
503 Service is currently inactive Temporarily disabled — contact support

Service-specific errors (DMT, AEPS) are listed at the bottom of their own sections: DMT Errors · AEPS Errors.

Quick Reference — All Endpoints

Method Endpoint Description
POST /api/v1/payin/create Create PayIn request
GET /api/v1/payin/status/{txn_ref} Check PayIn status
GET /api/v1/payin/transactions PayIn history
POST /api/v1/payout/process Initiate payout
GET /api/v1/transactions/service/payout Payout history
GET /api/v1/transactions/service/payout/{id} Single payout detail
POST /api/v1/dmt/remitter/profile DMT Step 1 — remitter profile check
POST /api/v1/dmt/remitter/register DMT Step 2 — register new remitter
POST /api/v1/dmt/remitter/verify-otp DMT Step 3 — verify remitter OTP
POST /api/v1/dmt/remitter/kyc DMT Step 4 — remitter eKYC
GET /api/v1/dmt/banks/search DMT — bank ID lookup
POST /api/v1/dmt/beneficiary/add DMT Step 5 — add beneficiary
POST /api/v1/dmt/beneficiary/verify-otp DMT Step 6 — verify beneficiary OTP
POST /api/v1/dmt/beneficiary/delete DMT — delete beneficiary (initiate)
POST /api/v1/dmt/beneficiary/delete-verify DMT — delete beneficiary (confirm)
POST /api/v1/dmt/transfer/generate-otp DMT Step 7 — generate transaction OTP
POST /api/v1/dmt/transfer DMT Step 8 — transfer
GET /api/v1/dmt/status/{transactionId} DMT — status check
POST /api/v1/merchant-onboarding/signup Onboard a new retailer/outlet
POST /api/v1/merchant-onboarding/biometric-kyc-status Check if biometric KYC is needed
POST /api/v1/merchant-onboarding/biometric-kyc Submit biometric KYC
POST /api/v1/merchant-onboarding/aeps-readiness Check bank-approval / AEPS readiness
POST /api/v1/aeps/banks Bank list (for bankiin)
POST /api/v1/aeps/outlet-login-status Check daily 2FA status
POST /api/v1/aeps/outlet-login Daily outlet 2FA
POST /api/v1/aeps/transaction-otp OTP for withdrawals above ₹5,000
POST /api/v1/aeps/cash-withdrawal Cash withdrawal
POST /api/v1/aeps/balance-enquiry Balance enquiry
POST /api/v1/aeps/mini-statement Mini statement
GET /api/v1/wallet/balance Wallet balance (available_balance + unsettled_balance)
GET /api/v1/wallet/transactions Full wallet ledger