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.
- Fund your merchant wallet from the NPS console.
- Call quote so your UI can show net payable (amount + charges + GST).
- Create a payout with a unique
client_id. - If the response is pending (TUP), wait for the webhook or poll status.
- 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-Key | Merchant public API key. Send only in this header, never in the URL. |
| X-Api-Secret | Merchant private secret. Send only in this header. Never put it in a browser or app. |
| X-Api-Timestamp | Current Unix time in seconds. Must be within 5 minutes of the server clock. |
| X-Api-Signature | HMAC-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
| amount | Required. 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_id | Required. Your unique reference. Must be unique per merchant and environment. |
| amount | Required. Amount credited to the beneficiary. Default range 1 to 49,500. |
| mobile_number | Required. 10-digit Indian mobile number. |
| beneficiary_name | Required. Alphabetic name only. |
| beneficiary_account | Required. Bank account number. |
| ifsc_code | Required. Valid 11-character IFSC. |
| transfer_mode | Required. IMPS, NEFT, RTGS, or UPI. |
| latitude | Optional. Defaults to 28.6139. |
| longitude | Optional. 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
| page | Optional. Default 1. |
| per_page | Optional. 10, 25, 50, or 100. |
| status | Optional. 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
- Read the raw JSON body exactly as received.
- Read
X-Nirmaata-Webhook-Timestamp. - Build
timestamp + "." + raw_json_payload. - HMAC-SHA256 with your webhook secret, prefix
sha256=. - Compare with
X-Nirmaata-Webhook-Signatureusing 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
| HTTP | statuscode | data.status | Meaning |
|---|---|---|---|
| 200 | TXN | SUCCESS | Beneficiary credited. Wallet debit kept. |
| 202 | TUP | PENDING | Under process. Wallet already debited. Wait for webhook or poll. |
| 403 | ERR | FAILED | Failed. Wallet refunded. |
Error codes
{
"success": false,
"error_code": "INSUFFICIENT_BALANCE",
"message": "Insufficient wallet balance"
}
| HTTP | Error code | Meaning |
|---|---|---|
| 401 | INVALID_API_CREDENTIALS | API key or secret is missing or wrong. |
| 401 | INVALID_SIGNATURE | Timestamp missing, too old, or HMAC does not match. |
| 403 | HTTPS_REQUIRED | API was called over HTTP. |
| 403 | IP_NOT_ALLOWED | Request IP is not in the merchant whitelist. |
| 413 | PAYLOAD_TOO_LARGE | JSON body is larger than 32 KB. |
| 415 | INVALID_CONTENT_TYPE | POST must use application/json. |
| 429 | RATE_LIMITED / AUTH_LOCKED | Too many requests or failed logins from this IP. |
| 403 | DOMAIN_NOT_ALLOWED | Request domain is not in the merchant whitelist. |
| 404 | PAYOUT_NOT_FOUND | No payout for this client_id or payout_id. |
| 409 | DUPLICATE_CLIENT_ID | client_id was already used. |
| 422 | VALIDATION_FAILED | Required field missing or invalid. |
| 422 | INVALID_AMOUNT | Amount is missing, zero, or below minimum. |
| 422 | AMOUNT_LIMIT_EXCEEDED | Amount is above the allowed maximum. |
| 422 | INVALID_TRANSFER_MODE | transfer_mode is not IMPS, NEFT, RTGS, or UPI. |
| 422 | INSUFFICIENT_BALANCE | Wallet cannot cover amount + charges + GST. |
| 500 | SERVER_ERROR | Temporary internal error. |
| 502 | UPSTREAM_ERROR | Banking provider request failed. |
| 503 | PROVIDER_NOT_CONFIGURED | Payout 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_idas 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);