๐Ÿ’ณ PayRails Merchant PayIn Integration Guide

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

EnvironmentLive Demo
Base URLhttps://api.payrails.in/api/v1
Authenticationx-api-key
Payment MethodUPI (Unified Payments Interface)
CurrencyINR (hardcoded by platform)

๐Ÿš€ 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 ← you are here
Create a live UPI transaction using the console on this page.
/index.php →
↓
๐Ÿ’ธ Test Payout
Create a live payout using the console on the Payout page.
/payout.php →
↓
๐Ÿ”” Receive Callback
Point your callback URL at the dedicated PAYIN Callback Lab endpoint to see a real inbound delivery, tagged with its Callback Type.
/callbacks/payin.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

โšก Quick Start

Everything you need to accept your first UPI payment in under 5 minutes.

๐Ÿ”
Step 1 — Get API Key
Obtain your x-api-key from PayRails. Include it on every request.
๐Ÿ“ค
Step 2 — Create Transaction
POST /transactions
Supply amount, merchantTxnId, paymentMethod.
๐Ÿ“ฅ
Step 3 — Receive Intent URL
Extract upi_intent_url from response. Redirect the customer.
๐Ÿ“ฑ
Step 4 — Customer Pays
Customer completes payment in their UPI app (GPay, PhonePe, Paytm, etc.).
๐Ÿ””
Step 5 — Receive Webhook
PayRails POSTs to your callback URL with the final status.
โœ…
Step 6 — Update Order
SUCCESS — fulfil the order.
FAILED — prompt retry.
Return HTTP 200 to webhook.

๐Ÿ”„ Integration Flow

Follow this sequence for every PayIn transaction.

๐Ÿ“ค POST /transactions
Send merchantTxnId, amount, paymentMethod, customer
↓
๐Ÿ“ฅ Receive upi_intent_url
transaction_id = PENDING  •  upi_intent_url in response
↓
๐Ÿ“ฑ Redirect Customer
Open UPI intent on mobile  •  Show QR on desktop
↓
โณ Customer Pays in UPI App
GPay  •  PhonePe  •  Paytm  •  BHIM and others
↓
๐Ÿ”” Webhook Received
PayRails POSTs to your callback URL  •  Return HTTP 200
↓
โœ… Terminal State
SUCCESS  •  FAILED  •  CANCELLED  •  EXPIRED

๐Ÿ“ค Create Transaction API

Initiates a UPI payment session. Returns a upi_intent_url that the merchant redirects the customer to.

Endpoint
POST /transactions
Auth
x-api-key
Success response
HTTP 201 Created
Initial status
PENDING
Currency
INR (hardcoded by platform)
Rate limit
Per merchant tier

Request Body

FieldRequiredTypeNotes
merchantTxnIdRequiredstringYour unique transaction reference. Must be unique per merchant.
amountRequirednumberTransaction amount in INR. Positive number.
paymentMethodRequiredstringMust be UPI for the merchant API.
customer.nameRequiredstringCustomer's name for logging and provider handoff.
customer.emailRequiredstringCustomer's email address.
customer.mobileRequiredstringCustomer's mobile number.
s2s_client_ipRecommendedstringCustomer's actual IP address. Used for fraud detection. Falls back to request IP if omitted.
s2s_device_infoRecommendedstringCustomer's browser User-Agent. Falls back to request header if omitted.
Not allowed: provider_id, provider_code, upi_vpa, routing_rule_id, routing_strategy โ€” these fields are rejected with HTTP 400.
Silently ignored: callbackUrl / callback_url โ€” configure your webhook URL in the PayRails merchant settings instead.

The live form below creates a real transaction using this exact endpoint.

๐Ÿš€ Live Demo

Enter an amount below and click Create UPI Transaction to execute a real API call. All debug panels below will populate with live data.

Transaction Amount (INR)

๐Ÿ“ฑ UPI Intent URL

The upi_intent_url field in the response is a deep link that opens the customer's UPI app directly. It follows the standard upi://pay?... URI scheme.

Format
upi://pay?pa=...&pn=...&am=...&tr=...&tn=...
Supported apps
  • Google Pay (GPay)
  • PhonePe
  • Paytm
  • BHIM
  • Amazon Pay
  • Any UPI-enabled bank app
Usage by platform
PlatformRecommended Approach
AndroidUse <a href="upi://pay?..."> or trigger via JavaScript. Opens UPI app selector automatically.
iOSSame deep link works on iOS. Not all UPI apps support iOS — GPay and PhonePe have iOS apps.
DesktopGenerate and display a QR code from the intent URL. Customer scans with phone.
Mobile webRedirect directly. Device opens UPI app selector.
Important: Never rely on the UPI app redirect alone to confirm payment. Always wait for the PayRails webhook before marking an order as paid. The customer may close the app, experience a network issue, or the payment may be delayed.

โ™ป๏ธ Transaction Status Lifecycle

A PayIn transaction transitions through these states after creation.

Standard flow
PENDING
Transaction created, awaiting payment
↓
SUCCESS
Customer completed UPI payment
Failure paths
PENDING → FAILED
Payment declined or error
PENDING → CANCELLED
Cancelled or EXPIRED (mapped to CANCELLED in webhook)

All statuses
PENDING SUCCESS FAILED CANCELLED EXPIRED
EXPIRED: The payment window elapsed without a definitive outcome from the provider. In webhook callbacks, EXPIRED is mapped to CANCELLED for simplicity. FAILED and SUCCESS are terminal states. Never trust a browser redirect for final status — always wait for the webhook.

๐Ÿ”” Webhook

PayRails sends a webhook to your configured callback URL when a transaction reaches a terminal state (SUCCESS, FAILED, CANCELLED/EXPIRED). Configure your callback URL in the PayRails merchant settings.

HTTP Method
POST
Payload format
application/json
Expected response
HTTP 200 (any 2xx)
Retry behaviour
Up to 3 attempts with exponential backoff
Webhook Payload
{
  "transaction_id":  "internal-uuid",
  "merchant_txn_id": "YOUR-REF-001",
  "status":          "SUCCESS",
  "amount":          "100",
  "currency":        "INR",
  "provider":        "razorpay",
  "provider_txn_id": "pay_xxxxx",
  "bank_reference":  "HDFC123456",
  "failure_code":    null,
  "failure_reason":  null,
  "completed_at":    "2026-07-08T..."
}
FieldTypeDescription
transaction_idstringPayRails internal UUID. Same as returned by Create Transaction.
merchant_txn_idstringYour original reference from the Create Transaction request.
statusstringSUCCESS ยท FAILED ยท CANCELLED
amountstringTransaction amount as stored (may be string).
currencystringAlways INR.
providerstringPayment provider that processed the transaction.
provider_txn_idstring|nullProvider's own transaction reference. Null if payment never reached the provider.
bank_referencestring|nullBank reference number (UTR). Available on SUCCESS for reconciliation.
failure_codestring|nullProvider-specific failure code. Null on SUCCESS.
failure_reasonstring|nullHuman-readable failure description. Null on SUCCESS.
completed_atstringISO-8601 UTC timestamp of webhook dispatch.
Your webhook handler must return HTTP 200 (any 2xx) within the timeout window. If it does not, PayRails retries up to 3 times. Implement idempotency in your handler using transaction_id to prevent double-fulfilment on retries.
Field naming note: the bank/UTR reference is named bank_reference in PayIn callbacks, but bank_reference_number in Payout callbacks (see the Payout Callback Behaviour 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 →

โš ๏ธ Error Reference

All errors from the PayIn backend. Every entry is verified from the actual implementation.

HTTPError / ConditionCauseResolution
400merchantTxnId is requiredmerchantTxnId missing from body.Always include a unique merchantTxnId.
400amount is requiredamount missing or zero.Send a positive numeric amount.
400paymentMethod is requiredpaymentMethod field absent.Send "paymentMethod": "UPI".
400provider_id/provider_code not allowedMerchant sent provider routing hints.Remove provider_id / provider_code from body.
400upi_vpa is not allowedMerchant sent a VPA in the request.Remove upi_vpa field.
400routing hints not allowedrouting_rule_id or routing_strategy present.Remove routing hint fields.
401UnauthorizedMissing or invalid x-api-key header.Include a valid API key.
409Duplicate merchantTxnIdA transaction with this merchantTxnId already exists for your merchant.Use a new unique reference.
503No provider availableNo payment provider is currently eligible for routing.Retry with backoff. Contact support if persistent.

๐Ÿ’ป cURL Reference Example

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

Create UPI Transaction

curl -X POST \
  'https://api.payrails.in/api/v1/transactions' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'x-api-key: YOUR_API_KEY' \
  -d '{
    "merchantTxnId": "ORDER-20261104112600",
    "amount": 100,
    "paymentMethod": "UPI",
    "customer": {
      "name": "Customer Name",
      "email": "customer@example.com",
      "mobile": "9876543210"
    },
    "s2s_client_ip": "1.2.3.4",
    "s2s_device_info": "Mozilla/5.0 ..."
  }'

โœ… Production Checklist

Verify each item before going live with PayRails PayIn API.

๐Ÿงช Test PayIn Live