💸 PayRails Merchant Payout Integration Guide

Live API console — authenticate, test, and explore the PayRails Payout API in real time.

EnvironmentLive Demo
Base URLhttps://api.payrails.in/api/v1
Authenticationx-api-key
Demo Amount₹100 (Fixed — server-side constant)
Payment ModeIMPS
BeneficiaryFixed test beneficiary (hardcoded on server)

🚀 Merchant Onboarding Journey

The complete path from zero to a production-ready PayRails integration — including webhook authentication, which is documented in full further down this page.

🧭 Getting Started
Read the Authentication section above and note your merchant environment (sandbox / staging / production).
#section-auth →
↓
🔑 Generate API Keys
Create an x-api-key for your environment from Developer Settings.
#section-api-keys →
↓
🔗 Configure Callback URLs
Tell PayRails where to POST PayIn and Payout callbacks.
#section-callback-url →
↓
🔐 Configure Webhook Authentication
A signing secret is created automatically the first time you configure a callback URL. Rotate or revoke it any time.
#section-webhook-auth →
↓
💳 Test PayIn
Create a live UPI transaction using the console on this page.
/index.php →
↓
💸 Test Payout ← you are here
Create a live payout using the console on the Payout page.
/payout.php →
↓
🔔 Receive Callback
Point your callback URL at the dedicated PAYOUT Callback Lab endpoint to see a real inbound delivery, tagged with its Callback Type.
/callbacks/payout.php →
↓
🔍 Inspect Payload
Review the received headers, raw body, and parsed JSON in the Callback Inspector.
#section-callback-lab →
↓
🛡️ Verify Signature
Confirm the HMAC signature using the same Callback Lab — or your own verifier from the HMAC guide below.
#section-hmac →
↓
🏗️ Implement Production Endpoint
Build your own receiver using the same verification formula, then point your real callback URL at it instead of the Callback Lab.
#section-hmac →
↓
✅ Go Live Checklist
Final review before switching to production traffic.
#section-checklist →

🔑 API Keys

API keys are managed from your PayRails merchant Dashboard under Developer Settings → API Keys. This section mirrors exactly what you will see there so you know what to expect before you go looking for it.

Environments
Sandbox · Staging · Production
Label
Optional — free text, for your own reference
Visibility
Shown once, at creation time, only
Dashboard table columns
Label · Environment · Active · Created · Last Used · Action

Generating a key

Click Generate API Key, choose an environment (sandbox, staging, or prod) and an optional label, then click Generate. The dashboard then shows:

This key cannot be shown again. Copy or download it now and store it securely.

Click Copy or Download, then I have saved this key to dismiss the dialog. The plaintext key is never displayable again after this point — if you lose it, generate a new one and revoke the old one.

Revoking a key

Click Revoke next to any active key. You will be asked to confirm:

Applications using it will immediately stop authenticating.
There is no separate “rotate” action for API keys. To rotate a key, generate a new one for the same environment, migrate your integration to the new key, then revoke the old key. (Webhook signing secrets, documented below, work differently — those do support an explicit Rotate action with a grace period.)

Once generated, use the key exactly as shown in the Authentication section above — in the x-api-key header on every request.

🔐 Authentication

Every request must include your merchant API key in the x-api-key header. No OAuth, no login session required.
Base URLhttps://api.payrails.in/api/v1
Auth methodx-api-key request header
Content typeapplication/json
ProtocolHTTPS only
Headers on every request
Content-Type: application/json
Accept: application/json
x-api-key: YOUR_API_KEY
X-Idempotency-Key: <uuid>  ← recommended
Idempotency: Include a unique X-Idempotency-Key per payout request. If you submit the same key within 48 hours and a payout was already created, the original response is returned immediately — no duplicate payout is created. Use a UUID or timestamp-based key generated server-side.

🔄 Integration Flow

Follow this sequence for every payout.

💰 Step 1 — Check Wallet Balance
GET /payouts/wallet  •  verify available_balance ≥ payout amount
↓
📤 Step 2 — Create Payout
POST /payouts  •  receive platform_payout_id in response
↓
📡 Step 3 — Poll Status
GET /payouts/{platform_payout_id}  •  repeat every few seconds
↓
✅ Step 4 — Terminal State
SUCCESS  •  FAILED  •  CANCELLED  •  EXPIRED

⚡ Quick Start

Everything you need to send your first payout in under 5 minutes.

🔐
Step 1 — Get API Key
Obtain your x-api-key from PayRails. Include it on every request.
💰
Step 2 — Check Wallet
GET /payouts/wallet
Verify available_balance ≥ payout amount.
📤
Step 3 — Create Payout
POST /payouts
Supply beneficiary details, amount, and mode.
📥
Step 4 — Save Payout ID
Store the platform_payout_id returned in the response. You will need it for status polling.
📡
Step 5 — Poll Status
GET /payouts/{platform_payout_id}
Repeat every 3–5 s until terminal state.
✅
Step 6 — Handle Outcome
SUCCESS — funds transferred and confirmed.
FAILED — permanently terminal. Log the failure_reason.
EXPIRED — contact PayRails support.

🛡️ Submission Guarantees & Status Retrieval

Understanding what happens after you submit a payout request — especially when you do not receive a response.

A network timeout does not indicate whether a payout was created or not. Always verify using the Status API before retrying a payout request.
Submission lifecycle
Merchant     │   POST /payouts     │──────────────── │   HTTP 201 ?
Your server ──────────────────────────────────────────────► PayRails API
✅ Response received
HTTP 201 → payout accepted
Continue with callbacks or Status API
⚠ No response received
Timeout / network error
Query Status API with merchant_ref
✅ Request Accepted

When the Create Payout API returns HTTP 201, the payout has already been accepted by PayRails. The response includes:

  • platform_payout_id
  • merchant_ref
  • status

From this point, determine the final payout outcome using either:

  • Your configured Callback URL
  • The Status API
The original POST /payouts response returns the initial status only. Never use it alone to determine final settlement.
⚠ Response Not Received

Timeout or Network Error

Sometimes your server may not receive the HTTP response due to:

  • Internet interruption
  • Reverse proxy timeout
  • Gateway timeout
  • Client disconnect
  • Network failure

This does not automatically mean the payout failed.

Query the Status API using your merchant_ref:

GET /payouts/status?merchant_ref=ORDER12345
HTTP 200 Payout already exists. Continue tracking with callbacks or Status API.
HTTP 404 Payout was never accepted. Safe to submit a new request using the same merchant_ref.
ℹ️ Important — Merchant Reference Numbers

Merchant reference numbers (merchant_ref) are unique per merchant. The same merchant_ref cannot be reused once a payout has been accepted.

Attempting to create another payout with an existing merchant_ref returns:

HTTP 409
DUPLICATE_MERCHANT_REFERENCE

This guarantees idempotent merchant-side integrations.

📬 Merchant Callback Behaviour

PayRails sends callbacks whenever a payout reaches a meaningful lifecycle event. Callbacks contain the latest payout status together with reconciliation information available at that moment.

Typical callback payload
FieldDescription
platform_request_idPlatform-generated idempotency reference
platform_payout_idUnique payout identifier (PPX…)
merchant_refYour original reference echoed back
statusCurrent payout status at time of callback
amountPayout amount in INR
currencyCurrency (INR)
fee_amountPlatform fee charged
modeTransfer mode (IMPS / NEFT / RTGS / UPI)
provider_codePayment provider used
provider_payout_idProvider-side transaction reference
provider_referenceProvider settlement reference (e.g. UTR)
bank_reference_numberRRN / UTR from the bank (when available)
provider_completed_atTimestamp of provider settlement (when available)
failure_reasonHuman-readable reason (for failed payouts)
Example callback payload
{
  "platform_request_id":   "PRQ...",
  "platform_payout_id":    "PPX...",
  "merchant_ref":          "ORDER12345",
  "status":                "SUCCESS",
  "amount":                1000,
  "currency":              "INR",
  "fee_amount":            5,
  "mode":                  "IMPS",
  "provider_code":         "bank_provider",
  "provider_payout_id":    "TXN98765",
  "provider_reference":    "IMPS/1234567",
  "bank_reference_number": "123456789012",
  "provider_completed_at": "2026-07-10T14:32:00Z",
  "failure_reason":        null,
  "timestamp":             "2026-07-10T14:32:05Z"
}
Bank Reference Number (UTR/RRN)
Some providers do not return a Bank Reference Number at the time of settlement. In those cases bank_reference_number will be null. This is provider behaviour — not an API error. You can retrieve the latest value at any time using the Status API.
Field naming note: the bank/UTR reference is named bank_reference_number in Payout callbacks, but bank_reference in PayIn callbacks (see the PayIn Webhook section). Both represent the same concept (the bank's UTR/RRN reconciliation reference) — the difference is naming only, not behaviour. This is existing, unchanged API behaviour; it is documented here so you do not need to guess which field name applies to which callback type.

🔗 Configure Callback URLs

Callback URLs are managed from your PayRails merchant Dashboard under Developer Settings → Webhook Callback. PAYIN and PAYOUT each have their own, independently configured callback URL (selected via a PAYIN / PAYOUT tab in the dashboard).

Scheme required
HTTPS only in production — plain HTTP is rejected
Blocked targets
Loopback / private / link-local addresses (SSRF protection)
Example URL
https://your-domain.com/webhooks/payrails

In the dashboard: select the PAYIN or PAYOUT tab, click Edit callback URL (or Configure if none is set yet), enter your URL, and click Save. If the URL fails validation the dashboard shows the exact rejection reason inline (for example, a malformed URL or a blocked/private address) — fix the URL and save again.

Once saved, the dashboard displays your current callback URL together with its type and the date it was registered, and offers Edit callback URL and Refresh actions.

Saving a callback URL for a type for the first time automatically provisions a webhook signing secret for that type — see Webhook Authentication below. There is no separate "enable signing" step.

Testing with the Callback Lab

While integrating, you can temporarily point your callback URLs at this portal's built-in Callback Lab to inspect real deliveries — headers, raw body, parsed JSON, and HMAC verification result — before your own endpoint is ready:

TypeCallback URL to configure
PayInhttps://dummy.payrails.in/callbacks/payin.php
Payouthttps://dummy.payrails.in/callbacks/payout.php

Developer Settings walkthrough

Merchant Dashboard
↓
Developer Settings
↓
Webhook Callback → PAYIN tab
↓
PAYIN Callback URL
https://dummy.payrails.in/callbacks/payin.php
Merchant Dashboard
↓
Developer Settings
↓
Webhook Callback → PAYOUT tab
↓
PAYOUT Callback URL
https://dummy.payrails.in/callbacks/payout.php

After saving: perform a test transaction (PayIn or Payout), open this Integration Portal, find the delivery in the Callback Inspector, verify its HMAC signature, compare the payload against the HMAC guide, and then update your own production implementation to match — finally, switch the callback URL over to your real endpoint once it's ready.

For the exact delivery behaviour (HTTP method, retry policy, expected response code, payload shape), see the Webhook section further down this page — that content is unchanged and still applies.

🔐 Webhook Authentication

Managed from your PayRails merchant Dashboard under Developer Settings → Webhook Authentication (PAYIN / PAYOUT tabs, same as Callback URL). This section documents the metadata and actions available there, exactly as they behave in the dashboard.

Metadata shown

FieldDescription
Key versionIncrements by 1 every time the secret is rotated. Also sent on every signed callback as the X-Payrails-Key-Version header.
Secret createdTimestamp the current callback type's secret was first provisioned.
Secret last rotatedTimestamp of the most recent rotation, or blank if never rotated.
Previous key expiresEnd of the grace period during which the previous secret is still accepted. Blank if there is no previous secret.
If no callback URL is configured yet for a type, the dashboard shows: "Configure a PAYIN callback URL first to enable webhook authentication." (or PAYOUT, depending on the selected tab) — configure the callback URL first (previous section), which auto-provisions the secret.

Rotate Secret

Click Rotate Secret. The dashboard asks you to confirm:

Rotate webhook secret?
This will generate a new webhook secret.
The current secret remains valid during the grace period.
Continue?

On confirmation, a new secret is generated immediately and shown exactly once:

This secret will not be shown again. Copy it now and store it securely.

Copy it, then click I have saved this secret. The previous secret (the one just replaced) remains valid for a 48-hour grace period — both the old and new secret will successfully verify signatures during that window, so you can redeploy your verifier with the new secret without dropping any in-flight callbacks.

Revoke Previous Secret

If you need to immediately invalidate the previous secret before its grace period naturally expires (for example, suspected compromise), click Revoke Previous Secret. This button is disabled with the tooltip "There is no previous secret to revoke." when there is nothing to revoke (i.e. you have never rotated, or the grace period already ended). Confirmation dialog:

Revoke previous secret?
This immediately invalidates the previous webhook secret.
Continue?
Revoking the previous secret does not generate a new secret and does not affect the current secret or key version — it only closes the grace period early.

The full HMAC signing scheme — headers, exact signing formula, and a PHP verification sample that supports both the current and previous secret — is documented in the HMAC Signature Verification section below.

🛡️ HMAC Signature Verification

Every signed callback includes an HMAC-SHA256 signature so you can verify it genuinely came from PayRails and was not tampered with in transit. This section is the authoritative reference for implementing that verification yourself. The PayIn and Payout Callback Labs (or the shared /callback-example.php) are working, runnable implementations of everything below.

Headers sent with a signed callback

HeaderExampleDescription
Content-Typeapplication/jsonAlways sent, signed or not.
X-Payrails-Signaturev1=3f2c9a...HMAC-SHA256 digest, hex-encoded, prefixed with the scheme version v1=.
X-Payrails-Timestamp1730678400Unix timestamp (seconds) captured when the callback was enqueued.
X-Payrails-Event-Id3d2f1a9e-...Unique UUID for this callback delivery.
X-Payrails-AlgorithmHMAC_SHA256Signing algorithm identifier.
X-Payrails-Key-Version2Which of your secrets (by rotation count) produced this signature.
If webhook authentication has not been configured yet for a merchant/callback type (no callback URL saved, or the platform's signing feature is not yet enabled for your account), only Content-Type is sent — there is no signature to verify. Treat an unsigned delivery as informational only until signing is enabled; do not reject it outright unless you require signing.

Exact signing formula

signing_input = raw_request_body + "." + X-Payrails-Timestamp + "." + X-Payrails-Event-Id
signature     = hex( HMAC_SHA256( secret, signing_input ) )
header value  = "v1=" + signature
Critical: raw_request_body means the exact, unmodified bytes of the HTTP request body — not a re-encoded/pretty-printed/re-ordered JSON string, and never PHP's $_POST superglobal (which is form-decoded and will not match). Always read the body with file_get_contents('php://input') before calling json_decode(), and compute the signature over those raw bytes.
The signature is stable across retries of the same delivery — the payload, event ID, and timestamp are frozen at the time the callback is first enqueued and are not regenerated on retry attempts, so the same signature will validate on every retry of that delivery.

PHP verification sample

Supports both the current and previous secret (for the 48-hour rotation grace period described in Webhook Authentication above). Uses hash_equals() for a timing-safe comparison — never compare signatures with == or ===.

📄 Full PHP verification sample (copy-paste ready)
<?php
// 1. Read the RAW body — never $_POST.
$rawBody = file_get_contents('php://input');

// 2. Read the PayRails headers.
$headers   = getallheaders();
$signature = $headers['X-Payrails-Signature'] ?? '';   // "v1=<hex>"
$timestamp = $headers['X-Payrails-Timestamp'] ?? '';
$eventId   = $headers['X-Payrails-Event-Id']  ?? '';

if (!preg_match('/^v1=([0-9a-f]{64})$/', $signature, $m)) {
    http_response_code(400);
    exit('Missing or malformed signature');
}
$providedHex = $m[1];

// 3. Rebuild the exact signing input.
$signingInput = $rawBody . '.' . $timestamp . '.' . $eventId;

// 4. Try the current secret, then the previous secret (grace period).
$currentSecret  = getenv('PAYRAILS_WEBHOOK_SECRET_CURRENT');
$previousSecret = getenv('PAYRAILS_WEBHOOK_SECRET_PREVIOUS'); // may be empty

$verified = false;
foreach (array_filter([$currentSecret, $previousSecret]) as $secret) {
    $expectedHex = hash_hmac('sha256', $signingInput, $secret);
    if (hash_equals($expectedHex, $providedHex)) {
        $verified = true;
        break;
    }
}

if (!$verified) {
    http_response_code(401);
    exit('Signature verification failed');
}

// 5. Only now is it safe to decode and act on the payload.
$payload = json_decode($rawBody, true);

// ... process $payload, using transaction_id / platform_payout_id for idempotency ...

http_response_code(200);
echo json_encode(['received' => true]);

Secret lifecycle

Secret auto-created
On first callback URL save for a type — key_version = 1
↓
Rotate Secret
key_version increments  •  old secret becomes "previous"
↓
Grace period (48h)
Both current and previous secrets verify successfully
↓
Grace period ends (or Revoke Previous Secret)
Only the current secret verifies from this point on

Troubleshooting

SymptomLikely cause
Signature never matchesVerifying against $_POST or a re-encoded JSON string instead of the raw php://input bytes.
Signature matched yesterday, fails todaySecret was rotated and the grace period has now expired — update your configured secret.
Works for new callbacks, fails for retriesShould not happen — the signature is stable across retries. If it does, you are likely recomputing the signing input from a re-parsed payload rather than caching the raw body.
No signature headers at allWebhook authentication has not been enabled/configured yet for this merchant/callback type — treat as unsigned.
Signature format doesn't match v1=<hex>Header was altered by a proxy/CDN, or you're reading the wrong header name.
Intermittent failures under loadConfirm you read the body exactly once per request — some frameworks buffer php://input such that a second read returns nothing.

Security recommendations

🔒 Security
  • Always verify the signature before parsing or acting on the payload.
  • Never log or expose your webhook secret — store it in server-side configuration only (environment variables, secrets manager), never in client-side code or version control.
  • Use HTTPS for your callback URL — it is required in production and enforced at configuration time.
  • Always use hash_equals() (or an equivalent constant-time comparison) — never ==/===, which are vulnerable to timing attacks.
  • Implement idempotency using transaction_id (PayIn) or platform_payout_id (Payout) — retries can deliver the same event more than once.
  • Log verification failures (without logging the secret itself) so you can investigate misconfiguration or genuine spoofing attempts.
  • Keep both the current and previous secret configured during a rotation window so in-flight deliveries are not rejected.

🧪 Callback Lab — Live Reference Receiver

A working, runnable callback receiver you can point a real (or simulated) callback URL at to see the entire HMAC verification flow in action — not just described, but executed.

Recommended: use the dedicated per-type endpoints below so the inspector always knows exactly which callback type it received — the shared /callback-example.php URL still works and infers the type from the payload shape instead.

Prefer one shared endpoint for both types? Open the canonical Callback Lab →

💡 Status API Best Practices

✅ Best Practice

Follow these practices to build a reliable payout integration.

  1. Always store both identifiers after payout creation:
    • merchant_ref — your own reference (used to query status after network failures)
    • platform_payout_id — PayRails identifier (used for all subsequent status lookups)
  2. Prefer callbacks for real-time updates. Configure a callback URL in your merchant account to receive push notifications when payout status changes.
  3. Use the Status API:
    • After any network failure or timeout
    • For end-of-day reconciliation
    • When callbacks are delayed or not yet received
    • Before retrying a payout — to confirm the original did not succeed
  4. Never assume a network timeout means the payout failed. Always verify using:
    GET /payouts/status?merchant_ref=YOUR_REF
    GET /payouts/status?platform_payout_id=PPX...
Retry safety: If the Status API returns HTTP 404 for your merchant_ref, the payout was never accepted and it is safe to submit a new request. If it returns HTTP 200, the payout exists — do not create a duplicate. Track the existing payout using its platform_payout_id.

💰 Wallet Balance API

Check your available balance before creating a payout. Attempting a payout with insufficient funds returns an error without creating any record.

Endpoint
GET /payouts/wallet
Auth
x-api-key
Response
HTTP 200 OK
Response Schema
{
  "currency":         "INR",
  "total_balance":    50000,
  "reserved_balance": 3000,
  "available_balance":47000,
  "payouts_enabled":  true,
  "is_frozen":        false
}
FieldTypeDescription
available_balancenumberImmediately usable funds. Formula: total_balance − reserved_balance.
reserved_balancenumberFunds locked for in-flight payouts not yet settled.
total_balancenumberFull balance including reserved funds.
payouts_enabledbooleanFalse when payouts are administratively disabled.
is_frozenbooleanTrue when wallet is frozen — all payout attempts will be rejected.

The live card below calls this exact endpoint and displays your real-time balance.

Wallet balance is checked automatically before each payout creation.

📤 Create Payout API

Initiates a real-money bank transfer. The payout begins processing immediately after successful creation.

Endpoint
POST /payouts
Auth
x-api-key
Success response
HTTP 201 Created
Idempotency
X-Idempotency-Key (optional, 1–255 chars)
Rate limit
10 requests / 60 seconds
Prerequisite
payout_mode = API_ENABLED

Request Body

FieldRequiredTypeValidation / Notes
amountRequirednumberPositive number. Must not exceed the merchant single-payout limit.
modeRequiredstringIMPS · NEFT · RTGS · UPI
beneficiary.nameRequiredstringNon-empty string.
beneficiary.account_numberRequiredstring6–18 digits. Encrypted before storage — never stored in plaintext.
beneficiary.ifscRequiredstring11-char IFSC code. Pattern: ABCD0123456
beneficiary.account_typeRequiredstringDefault SAVINGS. Allowed: SAVINGS CURRENT SALARY NRE NRO
beneficiary.bank_nameRequiredstringStored for reference. No validation.
beneficiary.mobileRequiredstringStored as-is. No format validation.
beneficiary.emailRequiredstringStored as-is. No format validation.
merchant_refRequiredstringYour reference ID. Not required to be unique.
This demo sends a fixed test beneficiary for safety. Your production integration supplies real beneficiary details in the same JSON structure above.

👤 Fixed Test Beneficiary

All payouts from this console go to the beneficiary below. These values are hardcoded constants on the server and cannot be changed from the browser.

Name
PayRails Test Payee
Bank
HDFC Bank
Account Number
XXXXXXXXX20090
IFSC Code
HDFC0001234
Account Type
SAVINGS
Email
support@payrails.in
Mobile
98XXXXXX21
Demo Amount
₹100

📡 Status Poll API

After creating a payout, use this endpoint to check its current status. Poll until the status reaches a terminal state.

Endpoint
GET /api/v1/payouts/status?merchant_ref=ORDER100

or

GET /api/v1/payouts/status?platform_payout_id=PPX...

Auth
x-api-key
Success response
HTTP 200 OK
Response Fields (API key path)
{
  "platform_payout_id":    "PPX...",
  "platform_request_id":   "PRQ...",
  "merchant_reference":    "DEMO_...",
  "status":                "SUCCESS",
  "amount":                100,
  "currency":              "INR",
  "mode":                  "IMPS",
  "provider_reference":    "COLT12345",
  "bank_reference_number": "HDFC9876",
  "failure_reason":        null,
  "created_at":            "...",
  "updated_at":            "..."
}

Rules: Supply exactly one lookup parameter. merchant_ref must be unique within your merchant account. The same response schema is returned regardless of lookup method.

Poll every 3–5 seconds. Stop immediately when status is a terminal value. The live polling card above uses this exact API automatically.

♻️ Payout Status Lifecycle

A payout transitions through these states. Poll the Status API until a terminal state is reached.

Standard flow (auto-approved)
PENDING
↓
PROCESSING
↓
SUCCESS  ✔
Approval required flow
PENDING
↓
APPROVAL_PENDING
↓
PROCESSING
↓
SUCCESS  ✔
Failure paths
PROCESSING → FAILED
PROCESSING → EXPIRED (timeout)
APPROVAL_PENDING → CANCELLED

All statuses
PENDING APPROVAL_PENDING PROCESSING SUCCESS FAILED CANCELLED EXPIRED REVERSED
Terminal states: FAILED CANCELLED EXPIRED REVERSED have no further transitions. SUCCESS is also terminal — once a payout succeeds, the only possible follow-on is REVERSED if the bank claws back the funds.

FAILED is permanent. The original payout row never changes after reaching FAILED. If an admin issues a retry, PayRails creates a brand-new payout with its own platform_payout_id and independent lifecycle — the original payout remains FAILED forever. Callbacks are sent separately for every payout attempt. Your system should track each payout by its own platform_payout_id.

EXPIRED means the polling window exceeded 24 h without a definitive outcome; wallet reservation is not automatically released.

👤 Beneficiary Handling

This integration guide uses inline beneficiary details supplied directly inside the POST /payouts request body. No separate beneficiary management step is required.

In your production integration, include the beneficiary's bank details in every payout request as shown in the Create Payout API section above. The fields you need are: name, account_number, ifsc, account_type, and optionally bank_name, mobile, and email. All values are encrypted before storage and the account number is never returned in plaintext.

⚠️ Error Reference

All errors from the payout backend. Every field is from the actual implementation.

HTTPError / ConditionCauseResolution
400PAYOUT_API_DISABLEDMerchant's payout_mode is not API_ENABLED.Contact PayRails support to enable API payouts.
400amount must be positiveamount is 0, negative, or not a number.Send a positive numeric amount.
400mode must be one of…Invalid mode value.Use IMPS, NEFT, RTGS, or UPI.
400beneficiary.account_number invalidNot 6–18 digits.Validate before submission.
400beneficiary.ifsc invalidDoes not match [A-Z]{4}0[A-Z0-9]{6}.Use a valid 11-char IFSC code.
400Insufficient balanceavailable_balance < amount.Check wallet before submitting.
400Wallet frozenWallet is frozen by admin.Contact PayRails support.
400Daily limit exceededMerchant daily payout limit reached.Wait for the next IST business day or request a limit increase.
400Mode not allowedRequested mode not in merchant's allowed list.Use a permitted mode or contact support.
403IP_NOT_ALLOWEDCaller IP not in the merchant IP whitelist.Add your server IP via the admin portal.
503IDEMPOTENCY_STORE_UNAVAILABLEIdempotency Redis store unavailable.Retry with backoff. Do not change your idempotency key.
503PAYOUTS_SYSTEM_DISABLEDGlobal kill-switch active.Contact PayRails support.
429Rate limit exceededMore than 10 payout creation requests in 60 seconds.Implement exponential backoff.

💻 cURL Reference Examples

Copy-paste ready examples. Replace YOUR_API_KEY with your actual key.

1. Check Wallet Balance

curl -X GET \\
  'https://api.payrails.in/api/v1/payouts/wallet' \\
  -H 'x-api-key: YOUR_API_KEY' \\
  -H 'Accept: application/json'

2. Create Payout

curl -X POST \\
  'https://api.payrails.in/api/v1/payouts' \\
  -H 'Content-Type: application/json' \\
  -H 'Accept: application/json' \\
  -H 'x-api-key: YOUR_API_KEY' \\
  -H 'X-Idempotency-Key: unique-request-id-here' \\
  -d '{
    "amount": 100,
    "beneficiary": {
      "name": "Beneficiary Name",
      "account_number": "55550100320090",
      "ifsc": "HDFC0001234",
      "account_type": "SAVINGS",
      "bank_name": "HDFC Bank",
      "mobile": "9876543210",
      "email": "payee@example.com"
    },
    "mode": "IMPS",
    "merchant_ref": "YOUR_REF_001"
  }'

3. Poll Payout Status

curl -X GET \\
  'https://api.payrails.in/api/v1/payouts/status?merchant_ref=ORDER100' \\
  -H 'x-api-key: YOUR_API_KEY' \\
  -H 'Accept: application/json'

or

curl -X GET \\
  'https://api.payrails.in/api/v1/payouts/status?platform_payout_id=PPX...' \\
  -H 'x-api-key: YOUR_API_KEY' \\
  -H 'Accept: application/json'

✅ Production Checklist

Verify each item before going live with PayRails Payout API.

🧪 Test Payout Live