Overview

Nirmaata Payout System (NPS) is an API-as-a-service for IMPS, NEFT, RTGS, and UPI payouts. Your server sends beneficiary details and amount. NPS checks your prepaid wallet, adds service charge and GST, and sends the payout to the banking provider.

  1. Fund your merchant wallet from the NPS console.
  2. Call quote so your UI can show net payable (amount + charges + GST).
  3. Create a payout with a unique client_id.
  4. If the response is pending (TUP), wait for the webhook or poll status.
  5. On FAILED, NPS refunds the wallet debit automatically.

Base URL: https://pyot.nirmaata.com

All API calls must be made from your backend. Do not put the API secret in a mobile app or browser.

Live keys debit the live wallet. Test keys debit the test wallet only. Use test keys until you are ready to go live.

Authentication

Every API request must include both credentials issued in the NPS console.

X-Api-Key: YOUR_API_KEY
X-Api-Secret: YOUR_API_SECRET
X-Api-Timestamp: 1732170000
X-Api-Signature: {hmac}
Content-Type: application/json
Accept: application/json
X-Api-KeyMerchant public API key. Send only in this header, never in the URL.
X-Api-SecretMerchant private secret. Send only in this header. Never put it in a browser or app.
X-Api-TimestampCurrent Unix time in seconds. Must be within 5 minutes of the server clock.
X-Api-SignatureHMAC-SHA256 of the request. Stops replay and body tampering.

Signature

canonical = timestamp + "\n" + METHOD + "\n" + path + "\n" + sha256(raw_body)
X-Api-Signature = hmac_sha256(canonical, api_secret)

METHOD is GET or POST. Path is the URL path only, for example /api/v1/payouts. For GET, raw body is empty and sha256 of empty string is used.

$timestamp = (string) time();
$body = json_encode($payload, JSON_UNESCAPED_SLASHES);
$canonical = $timestamp . "\nPOST\n/api/v1/payouts\n" . hash('sha256', $body);
$signature = hash_hmac('sha256', $canonical, $apiSecret);

Optional IP whitelist (single IPs or CIDR like 103.21.44.0/24) and domain whitelist can be set per merchant. Requests from other IPs are rejected.

After 10 failed auth attempts from one IP in 15 minutes, that IP is locked for 15 minutes. Payout create allows 180 requests per minute (about 3 per second) so a sustained 1 payout/second load is accepted.

cURL

TS=$(date +%s)
BODY='{"amount":10000}'
HASH=$(printf %s "$BODY" | sha256sum | awk '{print $1}')
SIG=$(printf '%s\nPOST\n/api/v1/payouts/quote\n%s' "$TS" "$HASH" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')
curl -X POST "https://pyot.nirmaata.com/api/v1/payouts/quote" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $API_KEY" \
  -H "X-Api-Secret: $API_SECRET" \
  -H "X-Api-Timestamp: $TS" \
  -H "X-Api-Signature: $SIG" \
  -d "$BODY"

Quote

Call this when the operator enters an amount and clicks next. It returns charges, GST, total debit, and whether the wallet can cover it. No money is moved.

POST /api/v1/payouts/quote

Request

amountRequired. Beneficiary payout amount in INR.
{ "amount": 10000 }

Response

{
  "success": true,
  "data": {
    "base_amount": "10000.00",
    "charge_percent": "0.7500",
    "charge_amount": "75.00",
    "gst_percent": "18.0000",
    "gst_amount": "13.50",
    "total_debit": "10088.50",
    "wallet_balance": "50000.00",
    "sufficient_balance": true,
    "shortfall": "0.00"
  }
}

Show total_debit as net payable. If sufficient_balance is false, ask the merchant to fund the wallet before creating the payout.

Create payout

POST /api/v1/payouts

Request body

client_idRequired. Your unique reference. Must be unique per merchant and environment.
amountRequired. Amount credited to the beneficiary. Default range 1 to 49,500.
mobile_numberRequired. 10-digit Indian mobile number.
beneficiary_nameRequired. Alphabetic name only.
beneficiary_accountRequired. Bank account number.
ifsc_codeRequired. Valid 11-character IFSC.
transfer_modeRequired. IMPS, NEFT, RTGS, or UPI.
latitudeOptional. Defaults to 28.6139.
longitudeOptional. Defaults to 77.2090.
{
  "client_id": "ORD-1001",
  "amount": 10000,
  "mobile_number": "9876543210",
  "beneficiary_name": "Amit Sharma",
  "beneficiary_account": "123456789012",
  "ifsc_code": "HDFC0001234",
  "transfer_mode": "IMPS",
  "latitude": "22.1234",
  "longitude": "72.1234"
}

Success (HTTP 200)

{
  "success": true,
  "status": "Success",
  "statuscode": "TXN",
  "message": "Transaction Successful",
  "data": {
    "payout_id": "NPS250921A1B2C3",
    "client_id": "ORD-1001",
    "amount": "10000.00",
    "charge_percent": "0.7500",
    "charges": "75.00",
    "gst_percent": "18.0000",
    "gst": "13.50",
    "total_debit": "10088.50",
    "transfer_mode": "IMPS",
    "mobile_number": "9876543210",
    "beneficiary_account": "123456789012",
    "beneficiary_name": "Amit Sharma",
    "ifsc_code": "HDFC0001234",
    "status": "SUCCESS",
    "op_id": "534523053608",
    "provider_reference": "348736748376472",
    "created_at": "2026-09-21 11:40:00",
    "updated_at": "2026-09-21 11:40:02"
  }
}

Pending (HTTP 202)

{
  "success": false,
  "status": "Pending",
  "statuscode": "TUP",
  "message": "Transaction Under Process",
  "data": { "...": "same data object, status PENDING" }
}

Wallet is already debited. Wait for the webhook or poll status. Do not create another payout with the same client_id.

Failed (HTTP 403)

{
  "success": false,
  "status": "Failed",
  "statuscode": "ERR",
  "message": "Transaction Failed",
  "data": { "...": "same data object, status FAILED" }
}

Wallet debit is refunded. You may retry with a new client_id.

Status

Look up by the client_id you sent, or by the NPS payout_id.

GET /api/v1/payouts/{client_id}
POST /api/v1/payouts/status
{ "client_id": "ORD-1001" }

GET /api/v1/payouts/id/{payout_id}

Response shape is the same as create payout. If the payout is still processing, NPS checks the provider before responding.

List payouts

GET /api/v1/payouts?page=1&per_page=25&status=success
pageOptional. Default 1.
per_pageOptional. 10, 25, 50, or 100.
statusOptional. success, pending, failed.
{
  "success": true,
  "data": [ { "payout_id": "NPS...", "client_id": "ORD-1001", "status": "SUCCESS" } ],
  "pagination": { "page": 1, "per_page": 25, "total": 120, "pages": 5 }
}

Wallet

GET /api/v1/wallet
{
  "success": true,
  "data": {
    "available_balance": "39911.50",
    "held_balance": "0.00",
    "updated_at": "2026-09-21 11:40:02"
  }
}

Live keys return the live wallet. Test keys return the test wallet. Funding the live wallet is done in the NPS console after a bank deposit.

Account verification

Optional. Confirms beneficiary name for an account number and IFSC before you send a payout.

POST /api/v1/verify-account
{
  "mobile_number": "9876543210",
  "beneficiary_account": "123456789012",
  "ifsc_code": "HDFC0001234",
  "latitude": "28.6139",
  "longitude": "77.2090"
}

Meta

GET /api/v1/meta

Returns active transfer modes and payout statuses.

Webhook

Set live and test callback URLs in the NPS merchant console. After the provider confirms SUCCESS or FAILED, NPS POSTs to your URL. Pending payouts do not fire a webhook.

Method: POST   Content-Type: application/json

Headers

Content-Type: application/json
X-Nirmaata-Webhook-Id: evt_xxx
X-Nirmaata-Webhook-Timestamp: 1780000000
X-Nirmaata-Webhook-Signature: sha256={hmac}

Success payload

{
  "status": "SUCCESS",
  "client_id": "ORD-1001",
  "payout_id": "NPS250921A1B2C3",
  "op_id": "534523053608",
  "amount": "10000.00",
  "charges": "75.00",
  "gst": "13.50",
  "total_debit": "10088.50",
  "transfer_mode": "IMPS",
  "message": "Transaction Processed Successfully",
  "event": "payout.success",
  "event_id": "evt_9f4b6e2d7c8a1b2c3d4e5f60",
  "environment": "live",
  "timestamp": "2026-09-21T11:40:00+05:30"
}

Failed payload

{
  "status": "FAILED",
  "client_id": "ORD-1001",
  "payout_id": "NPS250921A1B2C3",
  "op_id": "Refund For Order ID 1765977861904031",
  "amount": "10000.00",
  "charges": "75.00",
  "gst": "13.50",
  "total_debit": "10088.50",
  "transfer_mode": "IMPS",
  "message": "Transaction Failed",
  "event": "payout.failed",
  "event_id": "evt_9f4b6e2d7c8a1b2c3d4e5f60",
  "environment": "live",
  "timestamp": "2026-09-21T11:40:00+05:30"
}

client_id is the id you sent on create. amount is credited to the beneficiary. total_debit is taken from your wallet.

Verification

  1. Read the raw JSON body exactly as received.
  2. Read X-Nirmaata-Webhook-Timestamp.
  3. Build timestamp + "." + raw_json_payload.
  4. HMAC-SHA256 with your webhook secret, prefix sha256=.
  5. Compare with X-Nirmaata-Webhook-Signature using a timing-safe compare.
$signed = hash_hmac('sha256', $timestamp . '.' . $rawBody, $webhookSecret);
hash_equals('sha256=' . $signed, $headerSignature);

Respond with HTTP 2xx. Failed deliveries are retried. The same terminal status is not sent twice if the first delivery succeeded.

Charges

Default: 0.75% service charge on the payout amount, plus 18% GST on that charge. Both rates can be set per merchant in the console.

Example: payout 10,000 → charge 75.00 → GST 13.50 → wallet debit 10,088.50. Beneficiary receives 10,000.

Statuses

HTTPstatuscodedata.statusMeaning
200TXNSUCCESSBeneficiary credited. Wallet debit kept.
202TUPPENDINGUnder process. Wallet already debited. Wait for webhook or poll.
403ERRFAILEDFailed. Wallet refunded.

Error codes

{
  "success": false,
  "error_code": "INSUFFICIENT_BALANCE",
  "message": "Insufficient wallet balance"
}
HTTPError codeMeaning
401INVALID_API_CREDENTIALSAPI key or secret is missing or wrong.
401INVALID_SIGNATURETimestamp missing, too old, or HMAC does not match.
403HTTPS_REQUIREDAPI was called over HTTP.
403IP_NOT_ALLOWEDRequest IP is not in the merchant whitelist.
413PAYLOAD_TOO_LARGEJSON body is larger than 32 KB.
415INVALID_CONTENT_TYPEPOST must use application/json.
429RATE_LIMITED / AUTH_LOCKEDToo many requests or failed logins from this IP.
403DOMAIN_NOT_ALLOWEDRequest domain is not in the merchant whitelist.
404PAYOUT_NOT_FOUNDNo payout for this client_id or payout_id.
409DUPLICATE_CLIENT_IDclient_id was already used.
422VALIDATION_FAILEDRequired field missing or invalid.
422INVALID_AMOUNTAmount is missing, zero, or below minimum.
422AMOUNT_LIMIT_EXCEEDEDAmount is above the allowed maximum.
422INVALID_TRANSFER_MODEtransfer_mode is not IMPS, NEFT, RTGS, or UPI.
422INSUFFICIENT_BALANCEWallet cannot cover amount + charges + GST.
500SERVER_ERRORTemporary internal error.
502UPSTREAM_ERRORBanking provider request failed.
503PROVIDER_NOT_CONFIGUREDPayout provider is not ready.

Security

  • Call the API only from your server over HTTPS.
  • Never put API secret or webhook secret in frontend or mobile app code.
  • Verify every webhook signature before updating your order.
  • Treat client_id as idempotent. Do not reuse it.
  • Prefer webhook for terminal status. Use status poll as backup.
  • Restrict API keys with IP whitelist where possible.

PHP create payout

$body = json_encode([
  'client_id' => $clientId,
  'amount' => 10000,
  'mobile_number' => '9876543210',
  'beneficiary_name' => 'Amit Sharma',
  'beneficiary_account' => '123456789012',
  'ifsc_code' => 'HDFC0001234',
  'transfer_mode' => 'IMPS',
], JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp . "\nPOST\n/api/v1/payouts\n" . hash('sha256', $body), $apiSecret);
$ch = curl_init('https://pyot.nirmaata.com/api/v1/payouts');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
    'X-Api-Key: ' . $apiKey,
    'X-Api-Secret: ' . $apiSecret,
    'X-Api-Timestamp: ' . $timestamp,
    'X-Api-Signature: ' . $signature,
  ],
  CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);