/
v1.5 · 22 June 2026
No matches. Try a different keyword.
Payment Aggregator — Online · Payin / Collection APIs

iMoney PA-O Integration

Collect payments from your customers via UPI, QR, Intent, and Net Banking. The Payin APIs (also referred to as Collection APIs) handle the money-in side of your iMoney integration — encrypted by design, webhook-driven, with idempotent retries.

Live · last refreshed: Version · 1.5 Effective · 18 June 2026 Support · techpg@imoneypg.com

What is iMoney PA-O?

iMoney's Payment Aggregator — Online (PA-O) lets merchants collect digital payments through a single API. This guide covers the Payin service — internally also referred to as the Collection APIs — which handles the money-in flow. The integration follows a two-step Init → Execute flow, after which iMoney notifies your server of the final outcome via webhook.

Naming: "Payin APIs" and "Collection APIs" refer to the same set of endpoints. You'll see both terms used in our dashboard, contracts and onboarding documents — they are interchangeable.

🔐 Encrypted by default

Every request body uses AES-256-GCM. Plaintext is rejected. Your hash_key never leaves your server.

📡 Webhook source-of-truth

The webhook is the authoritative final state. Status check is a fallback only.

🧾 HTTP 200, always

Every API returns HTTP 200. Business outcomes live in response_code inside the JSON body.

⚖️ Built-in dispute flow

Chargebacks have a dedicated webhook track. Status APIs are blocked while a dispute is active.

Environments & Service URLs

Each service has its own dedicated subdomain on both UAT (sandbox) and Production (live). Use the relevant base URL for the API you're calling.

ServiceUAT (Sandbox)Production (Live)
Merchant Dashboarduatdashboard.imoneypg.comdashboard.imoneypg.com
Payin Open API
init & execute
uatpayin.imoneypg.compayin.imoneypg.com
Payin Webhook
callbacks originate from here
uatpayinhooks.imoneypg.compayinhooks.imoneypg.com
Payin Status Checkuatpayinstatus.imoneypg.compayinstatus.imoneypg.com

Note: Status check calls use a different base URL than init/execute. The Open API (init/execute) lives at payin.imoneypg.com, while the status check API lives at payinstatus.imoneypg.com. Webhooks reach you from payinhooks.imoneypg.com (still always sourced from one of the two trusted IPs).

Three rules that govern everything

⚠️ Read these before integrating
  1. Every API response returns HTTP 200. Never use HTTP status code to determine transaction outcome. Always read response_code inside the JSON body.
  2. Every request body must be AES-256-GCM encrypted. Plaintext requests are rejected.
  3. Webhook is the source of truth. The status check API is a fallback only.

Changelog #

A short release history for this guide. For breaking changes, a migration note is sent separately to tech@ contacts.

v1.5 22 June 2026 Current

NEW endpoint: Resend CallbackPOST /api/v3/payin/resend-callback/{pg_ref}/resend/. Manually re-trigger webhook delivery for a transaction whose webhook was missed. Rate-capped at 10 resend attempts per transaction. Blocked during disputes. Returns response_code: 000 with "queued for delivery" — the webhook arrives asynchronously at your registered URL.

Status check polling changed: previously "do not call before 5 minutes" — now available immediately after execute. You may poll every 5–10 seconds for up to 5 minutes while the customer is on the payment screen. The lifecycle diagram is updated to show ~5 sec rather than 5 min.

Webhook payload field renamed: payin_typepayin_method on the payin.status_update event. Update your handlers to look for data.payin_method rather than data.payin_type.

Decision Guide restructured — new "one rule" framing, a flowchart for the single most critical execute decision, and explicit guidance on when retrying Init is safe. The Master Decision Table now covers every signal source (Execute, Webhook, Status API, Resend, Support confirmation, no-webhook timeouts).

Per-section MD download — every section heading now has a small ⬇ MD button that downloads just that section's markdown (not the whole guide). Handy for sharing a specific topic with your team or pulling a section into an internal wiki.

v1.3 19 June 2026 Released

Major content update. New response codes: 100 (insufficient wallet balance), 115 (unrecognised payin_type), 116 (payin_type not enabled on selected route). HTTP exceptions documented: 103 uses HTTP 429; 101 uses HTTP 503 when the security key is briefly unavailable at gateway boot.

X-Signature is now required on init, status, and balance (previously: init only). Added the X-Merchant-Ref per-endpoint validation rules table. New routing_source codes (MERCHANT/PLATFORM) called out alongside Own-Route / Platform-Route.

Comprehensive Error Response tables added for every endpoint (Init Own-Route, Init Platform-Route, Execute, Status, Balance) — every documented scenario, code, and example message string. Includes the critical transaction_id consumption rule and the Payment Gateway ref:XXXXXXXX handling (do NOT retry; keep Pending; share ref with ops).

Field constraints clarified: all string fields capped at 256 chars; non-string types rejected as 104; customer_vpa is conditional on COLLECT payin_type; currency must be exact case.

Decision Guide updated to cover HTTP 429, HTTP 503, and the 101+ref: execute case — all map to "Keep Pending" with specific recovery steps.

v1.2.2 18 June 2026 Released

Added a new Edge / CloudFront Error Codes reference — documents the 4xx (400, 403, 404, 405, 414, 416) and 5xx (500, 501, 502, 503, 504) HTTP statuses returned by the CloudFront edge before requests reach iMoney's backend. Critical rule: never mark a transaction as Success or Failed when you receive one of these statuses — always verify with iMoney Operations first.

Removed the duplicate 403 row from Response Code Reference — geographic blocks are now exclusively covered by the new Edge / CloudFront table.

v1.2.1 18 June 2026 Released

Added a mandatory UAT Sign-off notice — every merchant must complete the UAT Sign-off Form with I Money Pay Private Limited before Production go-live. Without a completed sign-off, I Money Pay Private Limited and its Tech Team will not be held responsible for any losses or integration defects on the merchant side. Request the form from onboarding@imoneypay.in or techpg@imoneypg.com.

Added a Merchant Login button in the header — opens the iMoney Merchant Dashboard (dashboard.imoneypg.com/merchant/auth/login) in a new tab so merchants can sign in without losing their place in the docs.

v1.2 14 June 2026 Released

Renamed "Merchant Types" to "Routing Modes" to remove confusion with billing terminology. Prepaid → Own-Route and Non-Prepaid → Platform-Route. New explicit note: routing mode (which init endpoint to call) is independent of your billing type (prepaid wallet vs. postpaid settlement). Error 104 messages updated to use the new wording.

v1.1.2 10 June 2026 Released

Introduced 🇮🇳 India-Only Traffic Policy — in compliance with the Reserve Bank of India's Data Localization directive, iMoney accepts API traffic only from Indian IP addresses. VPN, proxy and any non-Indian-origin traffic is blocked at the edge with response_code: 403, even if all other authentication checks would otherwise pass. Host your application on a reputable cloud provider (Amazon AWS, Azure, or GCP) using their India region. This policy has been in effect from 10 June 2026 onward — merchants integrating after this date are bound by it.

v1.1.1 09 June 2026 Released

Documentation patch — no API contract change. Clarified that Payin APIs and Collection APIs refer to the same set of endpoints. Level 1 support email is now techpg@imoneypg.com (was tech@imoneypg.com). Mobile UX improvements (sticky header on iOS/Android, no horizontal scroll on phones, hamburger sidebar).

v1.1 02 June 2026 Released

Added Balance Check endpoint and the payin.test webhook event. X-Signature & X-Merchant-Ref are now required on every endpoint except execute; webhook retries reduced to 5 attempts; response code 109 repurposed for signature failure.

v1.0 26 May 2026 Archived

Initial release of the iMoney PA-O Payin Merchant Integration Guide — two-step Init → Execute flow, AES-256-GCM encryption, status check, full webhook & dispute model.

Quick Start #

Five steps to go from zero to a working integration.

1. Get your credentials

Onboard with iMoney to receive your api_key, hash_key, routing mode, and route name (Own-Route only). Whitelist iMoney's two static IPs on your server.

2. Implement encryption

SHA-256 key derivation + AES-256-GCM body encryption. Ready-to-use code in Python, Node.js, PHP and Java below.

3. Call Init → Execute

Step 1: POST /api/v3/payin/init/ (or /bank/init/). Step 2: POST /api/v3/payin/ returns the UPI link/QR.

4. Receive the webhook

Implement a webhook endpoint that decrypts the payload, verifies the source IP, and updates your transaction status. Always return HTTP 200 immediately.

5. Reconcile & go live

Validate Success / Failed / Pending logic against the Decision Guide. Switch BASE_URL to production, ship.

Credentials & Onboarding #

ItemDescriptionProvided By
api_keyYour merchant identifier, sent in every request headeriMoney Onboarding
hash_keyYour encryption secret — used to derive the AES key and signature. Never expose this.iMoney Onboarding
IP Whitelist (yours)Your server outbound IP(s) registered with iMoney — requests from unregistered IPs are rejectedMerchant provides, iMoney configures
Webhook URLYour HTTPS endpoint to receive payment status updatesMerchant provides, iMoney configures
Route Name(s)Assigned to your account — required only for Own-Route merchantsiMoney Onboarding
Routing ModeOwn-Route or Platform-Route — determines which init endpoint to use. Independent of your billing type.iMoney Onboarding
iMoney Sender IPsWhitelist these two on your firewall: 166.117.126.71, 99.83.230.145iMoney (always these two)
🔒 Security Notice

iMoney staff will never ask for your hash_key. Do not share it under any circumstances — store it in a secure secret manager (Vault, AWS Secrets Manager, etc.).

Routing Modes #

Your account is configured by iMoney for one routing mode, which determines which init endpoint you call. Calling the wrong init endpoint returns error 104.

Routing ModeDescriptionInit Endpointroute_name
Own-Route
routing_source = MERCHANT
You transact on your own acquirer account. You supply a route_name from your assigned MerchantAcquirerAccount, and iMoney routes through that specific acquirer. POST /api/v3/payin/init/ Required
Platform-Route
routing_source = PLATFORM
iMoney automatically selects the best available platform acquirer for the amount, currency, and payment type combination. POST /api/v3/payin/bank/init/ Not accepted

The execute endpoint is the same for both modes: POST /api/v3/payin/. If you are unsure of your routing mode, check your onboarding email or contact techpg@imoneypg.com.

⚠️ Routing mode and billing are independent

Your routing mode (which init endpoint you use) is a separate setting from your billing type (prepaid wallet vs. postpaid settlement). Do not infer one from the other — always use the endpoint that matches the routing mode iMoney configured for your account.

Security Model #

Every request passes through five gates in order:

Incoming Request
  │
  ├─ [1] API-KEY header        →  Identifies your merchant account
  ├─ [2] IP Whitelist          →  Request IP must be pre-registered with iMoney
  ├─ [3] X-Timestamp header    →  Unix timestamp, must be within ±5 minutes of server time
  ├─ [4] X-Signature header    →  HMAC integrity check (required on init endpoints)
  └─ [5] Encrypted body        →  AES-256-GCM payload confidentiality

Required headers

HeaderRequired OnFormatExample
API-KEYAll endpointsPlain string9b80b436a5ff
X-TimestampAll endpointsUnix seconds1716374400
X-SignatureInit, status, and balance endpointsLowercase hexa3f8c2…
X-Merchant-RefAll endpointsYour order/merchant referenceORDER-20260526-001
X-Customer-IPInit endpoints onlyCustomer's real IP203.0.113.45
Content-TypePOST endpointsFixedapplication/json

X-Merchant-Ref validation by endpoint

EndpointValidation
POST /init/ or /bank/init/Must exactly match merchant_ref in the encrypted body
POST /api/v3/payin/ (execute)Must match the merchant_ref from the init call for this transaction_id
GET /status/{pg_ref}/Validated against the merchant_ref stored on the transaction record
GET /status/merchant/{merchant_ref}/Must equal the URL path parameter
GET /balance/Must be present — any non-empty value accepted

Keep your server clock synced via NTP. Timestamp drift >±5 minutes returns response_code: 108.

Trusted iMoney IPs #

iMoney uses two Global Accelerator static IPs for ALL communication with merchant servers — both for callbacks we send to you and outbound API calls our system makes to you. Whitelist exactly these two:

166.117.126.71

iMoney GA endpoint #1 — primary sender for callbacks and API calls.

99.83.230.145

iMoney GA endpoint #2 — secondary sender, used for redundancy and failover.

🚨 Suspicious Activity

If you receive a callback or any request claiming to be from iMoney that originates from an IP other than 166.117.126.71 or 99.83.230.145, treat it as suspicious. Discard the request and report it immediately to techpg@imoneypg.com with the source IP, timestamp, and full headers.

India-Only Traffic Policy — RBI Data Localization #

🇮🇳 Compliance Notice — strictly enforced

iMoney accepts API traffic only from Indian IP addresses. In compliance with the Reserve Bank of India's Data Localization directive, all payment data must be stored and processed within India.

  • Calls to payin.imoneypg.com, payinstatus.imoneypg.com, and all other iMoney endpoints must originate from servers physically located in India.
  • VPN, proxy, anonymiser, and any non-Indian origin traffic is blocked at the edge — regardless of valid API keys / signatures.
  • Host your application on a reputable cloud provider with an Indian region — for example Amazon AWS, Microsoft Azure, or Google Cloud (GCP) — or any India-hosted data centre of your choice.
  • Whitelist only Indian outbound IPs on your end of the integration.

Requests originating from outside India will be silently dropped or returned with response_code: 403 — even if all other authentication checks would otherwise have passed.

Encryption #

All cryptographic operations are derived from your api_key + hash_key pair. Three steps: derive the key, encrypt the body, sign the request.

Key Derivation #

The 32-byte AES key is derived by SHA-256 hashing the concatenation of your api_key and hash_key.

import hashlib

def derive_key(api_key: str, hash_key: str) -> bytes:
    return hashlib.sha256(f"{api_key}{hash_key}".encode()).digest()

def derive_key_hex(api_key: str, hash_key: str) -> str:
    return hashlib.sha256(f"{api_key}{hash_key}".encode()).hexdigest()
const crypto = require('crypto');

function deriveKey(apiKey, hashKey) {
    return crypto.createHash('sha256').update(apiKey + hashKey).digest();
}
function deriveKeyHex(apiKey, hashKey) {
    return crypto.createHash('sha256').update(apiKey + hashKey).digest('hex');
}
function deriveKey(string $apiKey, string $hashKey): string {
    return hash('sha256', $apiKey . $hashKey, true);
}
function deriveKeyHex(string $apiKey, string $hashKey): string {
    return hash('sha256', $apiKey . $hashKey);
}
import java.security.MessageDigest;

public static byte[] deriveKey(String apiKey, String hashKey) throws Exception {
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    return md.digest((apiKey + hashKey).getBytes("UTF-8"));
}
public static String deriveKeyHex(String apiKey, String hashKey) throws Exception {
    byte[] raw = deriveKey(apiKey, hashKey);
    StringBuilder sb = new StringBuilder();
    for (byte b : raw) sb.append(String.format("%02x", b));
    return sb.toString();
}

Encrypting the Request Body #

All payloads must be AES-256-GCM encrypted and wrapped in a JSON envelope.

Wire format

nonce_hex : ciphertext_hex : tag_hex

Wrapped in JSON

{ "data": "a3f8c2...<nonce>:<ciphertext>:<tag>...hex" }
import os, json
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

def encrypt_payload(payload: dict, api_key: str, hash_key: str) -> bytes:
    key   = derive_key(api_key, hash_key)
    nonce = os.urandom(12)
    plain = json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()
    enc   = AESGCM(key).encrypt(nonce, plain, None)
    ct, tag = enc[:-16], enc[-16:]
    encrypted_str = f"{nonce.hex()}:{ct.hex()}:{tag.hex()}"
    return json.dumps({"data": encrypted_str}).encode()
function encryptPayload(payload, apiKey, hashKey) {
    const key    = deriveKey(apiKey, hashKey);
    const nonce  = crypto.randomBytes(12);
    const plain  = Buffer.from(JSON.stringify(payload), 'utf8');
    const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
    const ct     = Buffer.concat([cipher.update(plain), cipher.final()]);
    const tag    = cipher.getAuthTag();
    const encStr = `${nonce.toString('hex')}:${ct.toString('hex')}:${tag.toString('hex')}`;
    return Buffer.from(JSON.stringify({ data: encStr }));
}
function encryptPayload(array $payload, string $apiKey, string $hashKey): string {
    $key   = deriveKey($apiKey, $hashKey);
    $nonce = random_bytes(12);
    ksort($payload);
    $plain = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $ct    = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, '', 16);
    $encStr = bin2hex($nonce) . ':' . bin2hex($ct) . ':' . bin2hex($tag);
    return json_encode(['data' => $encStr]);
}
public static String encryptPayload(String json, String apiKey, String hashKey) throws Exception {
    byte[] key   = deriveKey(apiKey, hashKey);
    byte[] nonce = new byte[12];
    new SecureRandom().nextBytes(nonce);
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
    byte[] enc = cipher.doFinal(json.getBytes("UTF-8"));
    byte[] ct  = Arrays.copyOf(enc, enc.length - 16);
    byte[] tag = Arrays.copyOfRange(enc, enc.length - 16, enc.length);
    return "{\"data\":\"" + bytesToHex(nonce) + ":" + bytesToHex(ct) + ":" + bytesToHex(tag) + "\"}";
}

Computing the Signature #

The X-Signature header is required on all init, status, and balance endpoint calls. It proves that the request has not been tampered with in transit. (Execute calls rely on the AES-GCM auth-tag of the encrypted body for integrity and do not require a signature.)

Signature inputs by endpoint

Endpointref_idamountbody_bytes
POST /api/v3/payin/init/merchant_refe.g. "1000.00"encrypted request body
POST /api/v3/payin/bank/init/merchant_refe.g. "1000.00"encrypted request body
GET /api/v3/payin/status/<pg_ref>/pg_ref""b"" (empty)
GET /api/v3/payin/status/merchant/<merchant_ref>/merchant_ref""b"" (empty)
GET /api/v3/payin/balance/""""b"" (empty)

For GET endpoints body_bytes is empty, so body_hash = sha256(b"").hexdigest(). Use the formula in the code samples below — only the ref_id, amount, and body_bytes inputs differ per endpoint.

def compute_signature(api_key, hash_key, timestamp, merchant_ref, amount, body_bytes):
    key_hex   = derive_key_hex(api_key, hash_key)
    body_hash = hashlib.sha256(body_bytes).hexdigest()
    message   = f"{api_key}|{timestamp}|{merchant_ref}|{amount}|{body_hash}"
    return hashlib.sha256(f"{message}{key_hex}".encode()).hexdigest()
function computeSignature(apiKey, hashKey, timestamp, merchantRef, amount, bodyBuffer) {
    const keyHex   = deriveKeyHex(apiKey, hashKey);
    const bodyHash = crypto.createHash('sha256').update(bodyBuffer).digest('hex');
    const message  = `${apiKey}|${timestamp}|${merchantRef}|${amount}|${bodyHash}`;
    return crypto.createHash('sha256').update(message + keyHex).digest('hex');
}
function computeSignature($apiKey, $hashKey, $timestamp, $merchantRef, $amount, $bodyBytes): string {
    $keyHex   = deriveKeyHex($apiKey, $hashKey);
    $bodyHash = hash('sha256', $bodyBytes);
    $message  = "{$apiKey}|{$timestamp}|{$merchantRef}|{$amount}|{$bodyHash}";
    return hash('sha256', $message . $keyHex);
}
public static String computeSignature(String apiKey, String hashKey, String timestamp,
                                       String merchantRef, String amount, byte[] bodyBytes) throws Exception {
    String keyHex = deriveKeyHex(apiKey, hashKey);
    MessageDigest md = MessageDigest.getInstance("SHA-256");
    String bodyHash  = bytesToHex(md.digest(bodyBytes));
    String message   = apiKey + "|" + timestamp + "|" + merchantRef + "|" + amount + "|" + bodyHash;
    md.reset();
    return bytesToHex(md.digest((message + keyHex).getBytes("UTF-8")));
}

Full Request Example #

End-to-end payin init in Python — encrypts payload, computes signature, posts to iMoney.

import hashlib, json, os, time, requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

API_KEY  = "your_api_key"
HASH_KEY = "your_hash_key"
BASE_URL = "https://uatpayin.imoneypg.com"

def payin_init(payload: dict) -> dict:
    body      = encrypt_payload(payload, API_KEY, HASH_KEY)
    timestamp = str(int(time.time()))
    signature = compute_signature(API_KEY, HASH_KEY, timestamp,
                                   payload["merchant_ref"], payload["amount"], body)
    resp = requests.post(
        f"{BASE_URL}/api/v3/payin/init/",
        data=body,
        headers={
            "Content-Type":   "application/json",
            "API-KEY":        API_KEY,
            "X-Timestamp":    timestamp,
            "X-Signature":    signature,
            "X-Merchant-Ref": payload["merchant_ref"],
            "X-Customer-IP":  payload.get("customer_ip", ""),
        },
        timeout=30,
    )
    return resp.json()

Transaction Lifecycle #

Your System                   iMoney Gateway                  Customer
    │                              │                              │
    │── Step 1: Init ─────────────▶│                              │
    │◀── transaction_id + pg_ref ──│                              │
    │    (valid for 5 minutes)     │                              │
    │── Step 2: Execute ──────────▶│                              │
    │◀── response_code: 001 ───────│                              │
    │  [show QR / UPI link to customer]                           │
    │                              │◀──── customer pays ──────────│
    │◀── WEBHOOK ──────────────────│                              │
    │   [poll status after ~5 sec] │                              │
    │── Status Check ─────────────▶│                              │
    │◀── response_code: 000/003 ───│                              │

The webhook is the primary signal. Status check is available immediately after execute and may be polled to resolve uncertain states — but the webhook remains the authoritative final signal.

Step 1 — Init (Own-Route Merchants) #

POST/api/v3/payin/init/

Headers: API-KEY, X-Timestamp, X-Signature, X-Merchant-Ref, X-Customer-IP, Content-Type: application/json

Request Fields

FieldTypeRequiredValidation
uidstringYesYour internal customer identifier
merchant_refstringYesUnique per merchant, forever
amountstringYesPositive decimal, max 2 decimal places
currencystringYes"INR"
payin_typestringYesUPI, INTENT, QR, NETBANKING
route_namestringYesYour assigned route name
customer_namestringYesEnd customer's full name
customer_mobilestringYesEnd customer's mobile number
customer_emailstringNoEnd customer's email
customer_vpastringConditionalRequired if payin_type is COLLECT. Optional otherwise — stored on the transaction for fraud screening.
descriptionstringNoOrder description

All string fields must be sent as JSON strings. Sending a number where a string is expected returns response_code: 104.

String fields are capped at 256 characters. Longer values return response_code: 104.

Request

curl -X POST https://uatpayin.imoneypg.com/api/v3/payin/init/ \
  -H "API-KEY: 9b80b436a5ff" \
  -H "X-Timestamp: 1716374400" \
  -H "X-Signature: a3f8c2d1e5b7..." \
  -H "X-Merchant-Ref: ORDER-20260526-001" \
  -H "X-Customer-IP: 203.0.113.45" \
  -H "Content-Type: application/json" \
  -d '{"data":"<encrypted_string>"}'
{
  "uid": "CUSTOMER-001",
  "merchant_ref": "ORDER-20260526-001",
  "amount": "1000.00",
  "currency": "INR",
  "payin_type": "INTENT",
  "route_name": "YOUR_MERCHANT_ROUTE",
  "customer_name": "Raj Kumar",
  "customer_mobile": "9876543210",
  "customer_email": "raj@example.com",
  "customer_vpa": "raj@upi",
  "description": "Order #ORD-12345"
}

Response — Success

{
  "response_code": "000",
  "message": "OK",
  "data": {
    "transaction_id": "220260526143022123456",
    "pg_ref": "220260526143022ABCD",
    "expires_in": 300
  }
}

transaction_id is valid for 300 seconds. If it expires, call init again.

Error Responses — Init (Own-Route)

Scenarioresponse_codemessage (example)
Wrong routing mode — account is Platform-Route104"This endpoint is for merchants that route on their own acquirer. Platform-routed merchants must use /api/v3/payin/bank/init/."
Missing X-Merchant-Ref header104"X-Merchant-Ref header is missing."
Timestamp missing or outside ±5 min window108"X-Timestamp expired or missing. Use Unix timestamp ±5 min."
Rate limit exceeded (>10 req/s per IP)103"Rate limit exceeded — max 10 req/s per IP" · HTTP 429
Security key unavailable (transient)101"Security key unavailable. Retry in a moment." · HTTP 503
Body decryption failed110"Failed to decrypt request payload."
X-Signature verification failed109"X-Signature verification failed."
A field sent as non-string type104"Invalid type for field \"amount\": must be sent as a JSON string."
Field exceeds 256-character limit104"Field \"description\" exceeds maximum length of 256 characters."
Required field missing from body104"Missing required field(s): customer_mobile, customer_name"
customer_vpa missing for COLLECT type104"customer_vpa (UPI VPA) is required for COLLECT payment type."
Invalid amount format106"Invalid amount: ..."
Currency not supported or wrong case107"Currency \"inr\" not supported. Use exact format e.g. INR."
route_name does not exist for this merchant105"Route \"ROUTE_X\" not found for this merchant."
Route exists but is inactive105(route inactive message)
Route found but credentials not configured105"Route \"ROUTE_X\" is not fully configured (acquirer credentials missing). Contact support."
payin_type not recognised (system level)115"\"NETPAY\" is not a recognised payment type. Valid types: INTENT, NETBANKING, QR, UPI"
payin_type not enabled on this route116"\"NETBANKING\" is not enabled on route \"ROUTE_X\". Enabled on this route: INTENT, UPI"
Amount below route's minimum limit106"Amount 50.00 below minimum 100.00."
Amount exceeds route's per-transaction limit106"Amount 500000.00 exceeds per-transaction limit 100000.00."
Fraud engine (FUSE) unreachable101"Fraud screening service temporarily unavailable. Transaction blocked for safety."
Fraud engine blocked this transaction112"Transaction blocked due to fraud risk."
Insufficient prepaid wallet balance100"Insufficient wallet balance. Available: 50.00, required: 62.50 (estimated charges + 50 safety buffer)."
Postpaid wallet below safety floor100"Wallet balance below minimum safety floor. Available: -200.00, minimum required: 0."
Wallet not found for currency (prepaid)101"No active wallet for this currency."
Duplicate merchant_ref102"Duplicate merchant_ref — this reference already exists."

Step 1 — Init (Platform-Route Merchants) #

POST/api/v3/payin/bank/init/

This endpoint is for merchants configured with routing_source = PLATFORM. Same fields as Own-Route, except route_name is not accepted — iMoney auto-selects the best acquirer for the amount, currency, and payment type.

If your account is configured as Own-Route, calling this endpoint returns 104. Use Init (Own-Route) instead.

Error Responses — Init (Platform-Route)

All errors from Init (Own-Route) apply here with the same codes, except for these differences:

Scenarioresponse_codeNotes
Wrong routing mode — account is Own-Route104"This endpoint is for platform-routed merchants. Merchants with their own routes must use /api/v3/payin/init/ with route_name."
No platform route available for amount/currency/type combination105"No payin route available [ref:XXXXXXXX]. Contact admin with ref:XXXXXXXX." — include the ref code when contacting support
(no route_name, 115, 116 errors)Route is selected automatically; these don't apply
curl -X POST https://uatpayin.imoneypg.com/api/v3/payin/bank/init/ \
  -H "API-KEY: 197eaa2042e8" \
  -H "X-Timestamp: 1716374400" \
  -H "X-Signature: d4e6f8..." \
  -H "X-Merchant-Ref: ORDER-20260526-001" \
  -H "X-Customer-IP: 203.0.113.45" \
  -H "Content-Type: application/json" \
  -d '{"data":"<encrypted_string>"}'
{
  "uid": "CUSTOMER-001",
  "merchant_ref": "ORDER-20260526-001",
  "amount": "1000.00",
  "currency": "INR",
  "payin_type": "UPI",
  "customer_name": "Raj Kumar",
  "customer_mobile": "9876543210",
  "customer_email": "raj@example.com",
  "customer_vpa": "raj@upi",
  "description": "Order #ORD-12345"
}

Step 2 — Execute #

POST/api/v3/payin/

Headers: API-KEY, X-Timestamp, X-Merchant-Ref, Content-Type: application/json

No X-Signature on execute — the AES-GCM authentication tag on the encrypted body provides integrity.

X-Merchant-Ref must match the merchant_ref used in the init call for this transaction_id.

The execute response is AES-256-GCM encrypted. Decrypt the data field using the same key and wire format as Section 5.1–5.2. The examples below show the decrypted content.

Request Fields

FieldTypeRequiredRule
transaction_idstringYesFrom Step 1
amountstringYesMust exactly match Step 1
payin_typestringYesMust exactly match Step 1

Response — Pending Customer Action

{
  "response_code": "001",
  "message": "Transaction pending",
  "data": {
    "pg_ref": "PG20260526PAY001A",
    "transaction_id": "220260526143022123456",
    "payment_details": {
      "vpa": "merchant@upi",
      "qr_code": "<base64_or_svg_string>",
      "upi_link": "upi://pay?pa=merchant@upi&am=1000.00&cu=INR"
    }
  }
}

Show qr_code or upi_link to your customer. Do not mark as final. Store pg_ref for webhook reconciliation.

⚠️ transaction_id consumption rule: The transaction_id is consumed the moment it is used in execute — whether or not a mismatch error is returned. On any mismatch (amount, payin_type) you must generate a new merchant_ref and restart from Step 1.

Error Responses — Execute

Scenarioresponse_codemessage (example)
Rate limit exceeded103"Rate limit exceeded" · HTTP 429
Timestamp missing or outside ±5 min window108"X-Timestamp expired or missing."
Missing X-Merchant-Ref header104"X-Merchant-Ref header is missing."
Security key unavailable (transient)101"Security key unavailable. Retry in a moment." · HTTP 503
Body decryption failed110"Failed to decrypt request payload."
A field sent as non-string type104"Invalid type for field \"amount\": must be sent as a JSON string."
transaction_id missing from body111"transaction_id is required. Call /init/ first."
Required field missing104"Missing required field: amount"
Invalid amount format106"Invalid amount: ..."
transaction_id not found in DB111"transaction_id not found. Call /init/ first."
transaction_id already used (double execute)111"transaction_id has already been used. Your merchant_ref (ORDER-XXX) is reserved — generate a new merchant_ref and call /init/ again."
transaction_id expired (5-min window elapsed)111"transaction_id expired (5-minute window). Your merchant_ref (ORDER-XXX) is reserved — generate a new merchant_ref and call /init/ again."
X-Merchant-Ref does not match transaction104"X-Merchant-Ref header \"REF-A\" does not match the merchant_ref for this transaction."
amount does not match init amount106"Amount mismatch: init declared 1000.00, execute sent 900.00. This transaction_id is now consumed. Generate a new merchant_ref and call /init/ again with the correct amount."
payin_type does not match init type104"payin_type mismatch: init declared INTENT, execute sent UPI. This transaction_id is now consumed."
Merchant route credentials missing at execute time105"Merchant route is not fully configured (acquirer credentials missing)."
No acquirer linked to the payment record105"No acquirer configured for this payment."
No bank handler implemented for this acquirer+type105"No payment handler for AcquirerName_INTENT."
Payment gateway call raised an exception101"Payment gateway error [ref:XXXXXXXX]. Do NOT retry. Transaction (ORDER-XXX) remains Pending — share ref:XXXXXXXX with ops to investigate." · Do not retry. Keep Pending. Contact ops with the ref code.

Status Check #

Base URL is different from the Open API. Status check uses payinstatus.imoneypg.com (Production) or uatpayinstatus.imoneypg.com (UAT) — not the init/execute domain.

GEThttps://payinstatus.imoneypg.com/api/v3/payin/status/{pg_ref}/
GEThttps://payinstatus.imoneypg.com/api/v3/payin/status/merchant/{merchant_ref}/

Headers: API-KEY, X-Timestamp, X-Signature, X-Merchant-Ref

Signature formula for the pg_ref variant: ref_id = pg_ref, amount = "", body_bytes = b"".

Signature formula for the merchant_ref variant: ref_id = merchant_ref, amount = "", body_bytes = b"".

X-Merchant-Ref must match the merchant_ref on the transaction. For the status-by-pg_ref variant it is validated against the DB record; for status-by-merchant_ref it must equal the URL parameter.

Success responses are AES-256-GCM encrypted. Decrypt the data field using the same key and wire format as Section 5.1–5.2. Error responses (auth, timestamp, signature) are returned as plain JSON.

Request

curl -X GET "https://payinstatus.imoneypg.com/api/v3/payin/status/PG20260526PAY001A/" \
  -H "API-KEY: 9b80b436a5ff" \
  -H "X-Timestamp: 1716374700" \
  -H "X-Signature: <computed_sig>" \
  -H "X-Merchant-Ref: ORDER-20260526-001"

Response — Invalid Signature

{
  "response_code": "109",
  "message": "X-Signature verification failed."
}

Response — Success (decrypted)

{
  "response_code": "000",
  "message": "Transaction successful",
  "data": {
    "pg_ref": "PG20260526PAY001A",
    "merchant_ref": "ORDER-20260526-001",
    "status": "Success",
    "captured_amount": "1000.00",
    "customer_utr": "CUT20260526999"
  }
}
⚖️ Disputed transactions

Blocked for disputed transactions — returns response_code: 113. Await the payin.dispute_resolved webhook.

⏱️ Polling guidance (updated in v1.5)

Status check is available immediately after execute — you may call it as soon as ~5 seconds after Step 2. Poll every 5–10 seconds for up to 5 minutes while the customer is on the payment screen. After 5 minutes with no final status, use Resend Callback to force webhook re-delivery, then escalate if still unresolved after 30 minutes.

Resend Callback # NEW · v1.5

Use this endpoint to manually re-trigger webhook delivery for a transaction whose webhook was not received (e.g. your server was down, the URL was misconfigured, or all automatic retry attempts were exhausted).

POST/api/v3/payin/resend-callback/{pg_ref}/resend/

Headers: API-KEY, X-Timestamp, X-Merchant-Ref

Rate-capped at 10 resend attempts per transaction across all calls. Once the cap is reached, no further resends are accepted — escalate to support.

Blocked during disputes. If the transaction is under dispute, this endpoint returns response_code: 113. Use the dispute webhook flow instead.

Not a status check. This endpoint triggers a new webhook delivery — it does not return the transaction status inline. The webhook will arrive at your registered webhook URL.

Request

curl -X POST "https://payinstatus.imoneypg.com/api/v3/payin/resend-callback/PG20260526PAY001A/resend/" \
  -H "API-KEY: 9b80b436a5ff" \
  -H "X-Timestamp: 1716375000" \
  -H "X-Merchant-Ref: ORDER-20260526-001"

No request body required — the body may be empty or omitted.

Response — Accepted

{
  "response_code": "000",
  "message": "Callback queued for delivery.",
  "data": {
    "pg_ref": "PG20260526PAY001A"
  }
}

response_code: 000 means the webhook has been queued — not that it has been delivered. Allow up to 30 seconds for delivery to your endpoint.

Error Responses — Resend Callback

Scenarioresponse_codeNotes
Rate limit exceeded103HTTP 429
Timestamp missing or outside ±5 min window108Plain JSON
Missing X-Merchant-Ref header104Plain JSON
Security key unavailable (transient)101HTTP 503
pg_ref not found404"Transaction not found."
X-Merchant-Ref does not match the transaction104Plain JSON
Transaction is under dispute113"Transaction is under dispute. Resend callback is not available. Await payin.dispute_resolved webhook."
Resend cap reached (> 10 attempts)102"Resend limit reached for this transaction. Contact support."
Transaction not yet in a final state001"Transaction is still pending. Callback will be sent automatically when the transaction reaches a final state."

Action by Response

Response / ConditionWhat It MeansWhat to Do Next
response_code: 000Webhook queuedWait up to 30 seconds for it to arrive at your webhook endpoint. If it still doesn't arrive, check your endpoint is reachable and correct.
response_code: 001 — still pendingTransaction not yet finalDo not resend. The webhook will arrive automatically when the payment reaches a final state. Keep Pending.
response_code: 113 — under disputeDispute activeDo not resend. Await payin.dispute_resolved webhook.
response_code: 102 — cap reached10 resend limit exhaustedEscalate to iMoney support. Do not attempt further resends.
response_code: 404 — not foundWrong pg_refVerify the pg_ref value. Check your local records.
response_code: 104 — merchant_ref mismatchX-Merchant-Ref doesn't matchSend the same merchant_ref that was used during Init for this pg_ref.

Balance Check # NEW · v1.1

Read your current available balance held with iMoney. Useful for dashboards, reconciliation tooling, and balance monitoring.

GEThttps://payinstatus.imoneypg.com/api/v3/payin/balance/

Headers: API-KEY, X-Timestamp, X-Signature, X-Merchant-Ref

Signature formula: ref_id = "", amount = "", body_bytes = b"" — all three inputs are empty strings/bytes.

X-Merchant-Ref must be present, but any valid non-empty value is accepted — there is no transaction to validate against. We recommend a fixed marker such as BALANCE-CHECK.

The success response is AES-256-GCM encrypted. Decrypt the data field using Section 5.1–5.2.

Request

curl -X GET "https://payinstatus.imoneypg.com/api/v3/payin/balance/" \
  -H "API-KEY: 9b80b436a5ff" \
  -H "X-Timestamp: 1716374700" \
  -H "X-Signature: <computed_sig>" \
  -H "X-Merchant-Ref: BALANCE-CHECK"

Response — Success (decrypted)

{
  "status": "success",
  "response_code": "000",
  "data": {
    "balance": "25000.00",
    "currency": "INR"
  },
  "message": "",
  "timestamp": "1716374700"
}

Response — Authentication Error

{
  "status": "error",
  "response_code": "401",
  "data": null,
  "message": "API-KEY is not recognised.",
  "timestamp": "1716374700"
}

Response Code Reference #

All responses return HTTP 200, except:

  • HTTP 429response_code: 103 (rate limit)
  • HTTP 503response_code: 101 (security key unavailable at gateway boot)

Authentication failures (bad API-KEY, unregistered IP) are returned by the permission layer as HTTP 403 with a plain JSON error body — not a response_code in the business sense.

CodeMeaningEndpoint(s)Retryable?Action
000SuccessAllNoTerminal success — init/balance OK, or transaction is Success
001Pending — awaiting customer paymentExecuteNo — await webhookNormal post-execute state. Show payment screen. Await webhook.
002Initiated — customer started paymentStatus checkNo — await webhookIntermediate state. Keep pending. Await webhook.
003Failed — payment failed or rejectedStatus checkYes — new merchant_refTerminal failure confirmed. Allow customer to retry.
100Insufficient wallet balanceInitNoPrepaid: wallet doesn't cover estimated charges + buffer. Postpaid: balance below safety floor. Top up or contact ops.
101Internal / transient errorAllOnce after 60sRetry once, then contact support. Execute exception: do NOT retry — keep Pending and share the ref code with ops.
102Duplicate merchant_refInitNoReference already used. Generate a fresh unique merchant_ref.
103Rate limit exceededAll · HTTP 429Yes, reduce frequencySlow down. Max 10 req/s per IP.
104Invalid/missing field or header mismatchInit, ExecuteAfter fixRead message — it names the exact field and cause.
105Route unavailable or not configuredInit, ExecuteNoOwn-Route: route not found, inactive, or credentials missing. Platform-Route: no platform route matches. Contact iMoney Ops.
106Invalid amount or amount mismatchInit, ExecuteAfter fixAt init: format wrong, or below/above route limits. At execute: amount doesn't match init — transaction_id is consumed, start over.
107Currency not supported or wrong caseInitNoUse exact format e.g. INR (uppercase).
108Timestamp expired or missingAllYesSync NTP. ±5 min window.
109X-Signature verification failedInit, Status, BalanceAfter fixRecompute with the correct per-endpoint formula (see Signature).
110Decryption failedInit, ExecuteAfter fixVerify hash_key and your AES-256-GCM implementation.
111transaction_id invalid, expired, or already usedExecuteYes — re-initiateCall Step 1 with a new merchant_ref. Old merchant_ref may be reserved.
112Transaction blocked by fraud engineInitNoContact iMoney support. Do not retry automatically.
113Under dispute — status/resend blockedStatus checkNoAwait payin.dispute_resolved webhook.
115Payment type not recognisedInitYes — after fixpayin_type not in iMoney's system. Check valid types in the error message.
116Payment type not enabled on this routeInit (Own-Route only)NoYour route doesn't support this payin_type. Contact iMoney to enable it, or use a different type.
404Transaction not foundStatus checkNoVerify pg_ref or merchant_ref.

Edge / CloudFront Error Codes # NEW · v1.2.2

iMoney's APIs sit behind a CloudFront CDN edge. The HTTP status codes listed below are returned by CloudFront itself — they mean the request never reached our backend, so no response_code JSON body will be present. These are categorically different from the response_code values inside our normal JSON responses (Section Response Code Reference).

🛑 Critical handling rule

If you receive any of the HTTP statuses below, do NOT mark the transaction as Success or Failed on your side. CloudFront did not process the request, but in rare situations a transaction may still have been initiated elsewhere on the network. Always check with iMoney's Operations team before changing the transaction state. Reach out via techpg@imoneypg.com with merchant_ref, pg_ref (if you have it), the exact timestamp, and the full HTTP response.

4xx — Client-Side / Request Errors

HTTPMeaningWhat it usually meansAction
400Bad RequestMalformed HTTP request — invalid headers, malformed JSON envelope, broken encoding.Have your developer review the raw HTTP request; check headers, body bytes, and content-type.
403ForbiddenRequest was actively rejected at the edge — typically wrong API base URL, IP not allowed, or geographic restriction (see India-Only Policy).Verify base URL matches the environment (UAT vs Production) and that the request originates from an Indian IP.
404Not FoundThe endpoint path doesn't exist at the edge — likely a typo or a removed/renamed endpoint.Recheck the endpoint path. See Environments for current URLs.
405Method Not AllowedWrong HTTP verb (e.g. GET on a POST endpoint).Check the method in our docs — init/execute are POST; status/balance are GET.
414Request-URI Too LongThe URL itself exceeds CloudFront's limits — typically caused by giant query strings.Move parameters into the request body. Keep URLs short.
416Range Not SatisfiableA malformed Range header was sent.Remove the Range header — iMoney's APIs do not use range requests.

5xx — Edge / Upstream Errors

HTTPMeaningWhat it usually meansAction
500Internal Server ErrorCloudFront hit an unexpected internal error before reaching iMoney.Retry once after 60 seconds. If it persists, contact techpg@imoneypg.com.
501Not ImplementedCloudFront does not implement the requested HTTP feature (e.g. an unknown method).Use only standard methods (GET, POST). Contact support if you see this with a normal request.
502Bad GatewayiMoney's origin returned an invalid response to CloudFront — usually a deploy hiccup or upstream issue.Keep transaction as Pending. Wait 5 min, then call Status Check to confirm.
503Service UnavailableOrigin is temporarily down or rate-limited.Keep transaction as Pending. Back off, retry after a few minutes, then verify via Status Check.
504Gateway TimeoutOrigin took too long to respond — request may or may not have been processed.Keep transaction as Pending. Wait 5 min, then verify via Status Check. Do not retry an execute call.
⚠️ Reminder: HTTP status ≠ response_code

The codes above are HTTP status codes emitted at the CloudFront edge — they are different from the response_code values you'll find inside a normal iMoney JSON response (which always travel on HTTP 200). Don't confuse the two when wiring up your error-handling switch-cases.

Transaction Status Reference #

StatusMeaningTerminal?Your Action
PendingAwaiting customer actionNoKeep Pending, await webhook
InitiatedCustomer started payment flowNoKeep Pending, await webhook
SuccessPayment received and confirmedYesRelease order / credit customer
FailedPayment failed or rejectedYesAllow customer retry
ExpiredCustomer didn't pay in timeYesAllow retry

When to Mark Success, Failed, or Pending #

The one rule: only a webhook carrying a final transaction_status, or explicit written confirmation from iMoney support, may move a transaction out of Pending. Everything else — errors, timeouts, unfamiliar codes, your own exceptions — keeps the transaction Pending until resolved.

The One Decision You Must Get Right — Execute

Execute called
      │
      ├── Got response_code + message?
      │       │
      │       ├── 001 ──────────────────────────────────────▶ PENDING — show UI, await webhook
      │       ├── 104/106/110/111 (not "already used") ─────▶ FAILED — no payment, restart with new merchant_ref
      │       ├── 101 with ref:XXXXXXXX ───────────────────▶ PENDING — contact ops, do NOT retry
      │       └── anything else ──────────────────────────▶ PENDING — check status after ~5 seconds
      │
      └── No response (timeout / 5xx / your exception)?
              │
              └─────────────────────────────────────────────▶ PENDING — check status after ~5 seconds, do NOT retry execute

When Retry Init Is Safe

You may only call Init again (with a new merchant_ref) when you have confirmed one of:

In all other cases, keep Pending and investigate before creating a new transaction.

Master Decision Table — By Signal Source

Definitive guide for every signal your integration will encounter.

ScenarioMark AsNotes
Execute returns 001PendingStandard post-execute. Await webhook.
Execute returns 102FailedDuplicate ref — definitive.
Execute returns 104FailedInvalid field — fix and retry.
Execute returns 111FailedRe-init with fresh merchant_ref.
Execute returns 110FailedFix encryption.
Other unknown errorPendingUncertain — contact support.
Execute returns 101 with ref:XXXXXXXXPendingPayment gateway error. Do NOT retry. Contact ops with the ref code. Transaction remains Pending.
HTTP 429 from gatewayPendingRate limit — slow down and retry. Do not mark Failed.
HTTP 503 from gatewayPendingGateway key unavailable. Retry after 60 seconds.
HTTP 500 / 502 / 504 from gatewayPendingInfrastructure error. Do not retry. Check status after 5 minutes.
Connection timeoutPendingUnknown if request was received.
Webhook: SuccessSuccessAuthoritative. Release order.
Webhook: FailedFailedAllow retry with new order.
Webhook: ExpiredExpiredAllow retry.
Status returns 000SuccessUse as reconciliation.
No webhook after 24hEscalateContact techpg@imoneypg.com.

Key rule: Only a webhook with a final transaction_status or explicit iMoney support confirmation moves a transaction out of Pending.

Unexpected Scenarios — Always Keep Pending #

When something unexpected happens — network error, ambiguous response, exception in your code, timeout — always keep the transaction Pending.

⚠️ Why this matters

iMoney may have processed the request even if you didn't get a response. Marking Failed and allowing a retry could double-charge your customer.

Keep transaction as Pending

Persist the state. Do not auto-fail.

Record details

Save merchant_ref and the exact error.

Wait 5 minutes

Then call status check.

Reconcile

000 → Success; 003 → Failed; not found → safe to retry.

Escalate if still uncertain

Contact iMoney support with merchant_ref and timestamp.

Webhook Integration #

Delivery Mechanism

When a payin reaches a final state, iMoney sends an encrypted HTTP POST to your webhook URL.

Webhook Payload #

iMoney sends a POST with these headers:

Content-Type: application/json
X-Webhook-Timestamp: <unix_epoch>
X-Pg-Ref: <pg_ref>
X-Api-Key: <your_api_key>
X-Merchant-Ref: <your_merchant_ref>

Raw body

{
  "encrypted": "<nonce_hex>:<ciphertext_hex>:<tag_hex>",
  "timestamp": "1716374800",
  "pg_ref": "PG20260526PAY001A"
}

Decrypted payload — Success

{
  "event": "payin.status_update",
  "data": {
    "pg_ref": "PG20260526PAY001A",
    "merchant_ref": "ORDER-20260526-001",
    "transaction_status": "Success",
    "amount": "1000.00",
    "captured_amount": "1000.00",
    "payin_method": "INTENT",
    "customer_utr": "CUT20260526999",
    "customer_upi": "user@upi",
    "bank_ref": "UTR123456789",
    "acquirer_ref": "ACQ20260526001",
    "currency": "INR",
    "created_at": "2026-05-26T10:05:00+05:30",
    "updated_at": "2026-05-26T10:05:30+05:30"
  }
}

Event: payin.test — Test Webhook NEW · v1.1

Fired from the iMoney merchant dashboard when you use the "Send Test Webhook" function. No action is required — just decrypt to confirm your endpoint is wired correctly, then return HTTP 200.

{
  "event": "payin.test",
  "data": {
    "pg_ref": "TEST-PG-REF",
    "merchant_ref": "TEST-MERCHANT-REF",
    "message": "This is a test webhook from iMoney. No action required."
  }
}

Use this to validate your webhook plumbing (TCP whitelisting, decryption, ack with HTTP 200) without affecting any real order.

Decrypted payload fields

FieldTypeDescription
pg_refstringiMoney's unique transaction reference
merchant_refstringYour order reference from the init request
transaction_statusstringFinal status: Success, Failed, or Expired
amountstringThe amount from the init request
captured_amountstringActual amount received — may differ. Use this for reconciliation.
payin_method RENAMED in v1.5stringPayment method used by the customer: UPI, INTENT, QR, NETBANKING (was payin_type in v1.4 and earlier)
customer_utrstringUTR/reference from the customer's bank — empty if payment did not complete
customer_upistringCustomer's UPI VPA — empty if not collected
bank_refstringiMoney's bank-side reference — empty if payment did not complete
acquirer_ref NEWstringAcquiring bank reference — empty if payment did not complete
currencystringCurrency code
created_atstringTransaction creation time (ISO 8601)
updated_atstringLast update time (ISO 8601)

Acknowledging the Webhook #

Your endpoint must return HTTP 200 immediately — before processing.

Correct pattern: acknowledge first, process asynchronously.

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def payin_webhook(request):
    job_queue.enqueue(process_webhook, request.body)
    return HttpResponse("OK", status=200)
app.post('/webhook/payin', express.raw({ type: 'application/json' }), (req, res) => {
    jobQueue.add({ body: req.body });
    res.status(200).send('OK');
});
<?php
$body = file_get_contents('php://input');
enqueueWebhook($body);
http_response_code(200);
echo 'OK';
exit;

Trusting & Verifying the Webhook #

Two verification layers — use both:

1

Source IP verification

Only accept POSTs from 166.117.126.71 or 99.83.230.145. Any other IP is suspicious — drop and report.

2

Payload decryption

AES-256-GCM is authenticated — decryption fails on tampering. If it succeeds, the payload is authentic.

Verification checklist

FUNCTION verify_and_process_webhook(source_ip, raw_body):

    // Step 1: IP check — only iMoney's two GA IPs
    IMONEY_TRUSTED_IPS = ["166.117.126.71", "99.83.230.145"]
    IF source_ip NOT IN IMONEY_TRUSTED_IPS:
        LOG "SUSPICIOUS: webhook from " + source_ip
        ALERT techpg@imoneypg.com
        RETURN HTTP 403

    // Step 2: Parse + decrypt
    envelope = JSON_DECODE(raw_body)
    TRY:
        payload = decrypt_webhook(envelope["encrypted"], api_key, hash_key)
    CATCH DecryptionError:
        LOG "Decryption failed — possible tampering"
        RETURN HTTP 200   // stop retries, do not process

    // Step 3: Idempotency
    txn = database.find(payload["data"]["pg_ref"])
    IF txn is None OR txn.is_final_state():
        RETURN HTTP 200

    // Step 4: Process
    UPDATE txn.status = payload["data"]["transaction_status"]
    IF status == "Success": release_order(txn)
    ELSE: allow_retry(txn)

    RETURN HTTP 200

Decrypting the Webhook #

Same key derivation and wire format as outbound requests.

def decrypt_webhook(encrypted_str, api_key, hash_key):
    nonce_hex, ct_hex, tag_hex = encrypted_str.strip().split(':')
    key   = derive_key(api_key, hash_key)
    nonce = bytes.fromhex(nonce_hex)
    ct    = bytes.fromhex(ct_hex)
    tag   = bytes.fromhex(tag_hex)
    plain = AESGCM(key).decrypt(nonce, ct + tag, None)
    return json.loads(plain)
function decryptWebhook(encryptedStr, apiKey, hashKey) {
    const [nh, ch, th] = encryptedStr.trim().split(':');
    const nonce = Buffer.from(nh, 'hex');
    const ct    = Buffer.from(ch, 'hex');
    const tag   = Buffer.from(th, 'hex');
    const key   = deriveKey(apiKey, hashKey);
    const d     = crypto.createDecipheriv('aes-256-gcm', key, nonce);
    d.setAuthTag(tag);
    return JSON.parse(Buffer.concat([d.update(ct), d.final()]).toString());
}
function decryptWebhook($encryptedStr, $apiKey, $hashKey): array {
    [$nh, $ch, $th] = explode(':', trim($encryptedStr));
    $key = deriveKey($apiKey, $hashKey);
    $plain = openssl_decrypt(hex2bin($ch), 'aes-256-gcm', $key,
                             OPENSSL_RAW_DATA, hex2bin($nh), hex2bin($th));
    if ($plain === false) throw new Exception('Decryption failed');
    return json_decode($plain, true);
}
public static Map decryptWebhook(String encryptedStr, String apiKey, String hashKey) throws Exception {
    String[] parts = encryptedStr.trim().split(":");
    byte[] nonce = hexToBytes(parts[0]);
    byte[] ct    = hexToBytes(parts[1]);
    byte[] tag   = hexToBytes(parts[2]);
    byte[] key   = deriveKey(apiKey, hashKey);
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
    byte[] ctTag = new byte[ct.length + tag.length];
    System.arraycopy(ct, 0, ctTag, 0, ct.length);
    System.arraycopy(tag, 0, ctTag, ct.length, tag.length);
    return parseJson(new String(cipher.doFinal(ctTag), "UTF-8"));
}

Dispute Webhook Flow #

How Disputes Differ from Normal Flow

A dispute opens when a previously-terminal transaction (e.g. Success) is contested. Runs on a separate webhook track.

Key rules
  • No further normal-flow webhooks for the disputed pg_ref.
  • Dispute events: payin.dispute_raised and payin.dispute_resolved.
  • Status check is blocked → returns 113.
  • Resend callback is blocked → returns 113.
  • Same encryption, same trusted IPs (166.117.126.71, 99.83.230.145), same HTTP 200 ack.

Dispute Raised Webhook #

X-Webhook-Timestamp: <unix_epoch>
X-Pg-Ref: <pg_ref>
X-Api-Key: <your_api_key>
X-Merchant-Ref: <your_merchant_ref>
X-Dispute-Id: <dispute_id>
X-Event: payin.dispute_raised
{
  "event": "payin.dispute_raised",
  "data": {
    "pg_ref": "PG20260526PAY001A",
    "merchant_ref": "ORDER-20260526-001",
    "dispute_id": "DSP20260526001",
    "dispute_reason": "Customer claims payment not received",
    "dispute_category": "CHARGEBACK",
    "previous_status": "Success",
    "current_status": "Dispute",
    "amount": "1000.00",
    "currency": "INR",
    "raised_at": "2026-05-26T14:00:00+05:30"
  }
}

Dispute Resolved Webhook #

Carries the financial settlement detail.

Merchant Loss (chargeback upheld)

{
  "event": "payin.dispute_resolved",
  "data": {
    "pg_ref": "PG20260526PAY001A",
    "dispute_id": "DSP20260526001",
    "previous_status": "Dispute",
    "current_status": "Failed",
    "resolution": "MERCHANT_LOSS",
    "resolution_remark": "Chargeback upheld by customer bank",
    "amount": "1000.00",
    "not_settled_amount": "1000.00",
    "charges": "25.00",
    "debit": "1025.00",
    "credit": "0.00",
    "currency": "INR",
    "resolved_at": "2026-05-28T10:30:00+05:30"
  }
}

Merchant Win (dispute rejected)

{
  "event": "payin.dispute_resolved",
  "data": {
    "pg_ref": "PG20260526PAY001A",
    "dispute_id": "DSP20260526001",
    "previous_status": "Dispute",
    "current_status": "Success",
    "resolution": "MERCHANT_WIN",
    "resolution_remark": "Dispute rejected — original payment confirmed valid",
    "amount": "1000.00",
    "not_settled_amount": "0.00",
    "charges": "0.00",
    "debit": "0.00",
    "credit": "1000.00",
    "currency": "INR",
    "resolved_at": "2026-05-28T10:30:00+05:30"
  }
}

When to Mark Status During a Dispute #

EventMark AsNotes
payin.dispute_raisedDisputeOverrides previous status.
Resolved + SuccessSuccessMerchant win.
Resolved + FailedFailedMerchant loss. Record debit/charges.
Resolved + PartialPartialRecord not_settled_amount.
No resolved after 7 daysEscalateContact support with dispute_id.

Escalation & Support #

When escalating, always provide: merchant_ref, pg_ref, exact timestamp, and the full response/error received.

Level 1 Support

techpg@imoneypg.com

Integration help, webhooks, encryption, general technical issues.

Level 2 Support

tech1@imoneypg.com

Escalations from L1, stuck transactions, urgent production issues.

Merchant Onboarding

onboarding@imoneypay.in

New merchant onboarding, KYC, account activation, routes.

Customer Care

care@imoneypay.in

Customer-facing queries, payment confirmations, general support.

🌐 Website

www.imoneypay.in — product information, pricing, contact forms.

🚨 Suspicious Activity

If you receive a request claiming to be from iMoney from an IP other than 166.117.126.71 or 99.83.230.145, report it immediately to techpg@imoneypg.com with the source IP and full headers.

🛟 Tech Safety — when something isn't covered here

If you hit a scenario, error message, or edge-case that isn't documented above, do not improvise — contact our support team immediately at techpg@imoneypg.com for a fast resolution. Payment systems evolve and surprising things sometimes happen on the network or acquirer side. Always keep your integration up to date — whenever we change anything in the API surface, this documentation is updated first and a new entry is published in the Changelog with the new version number. Subscribe to techpg@imoneypg.com's release mailing list to be notified the moment a new version ships.

🛑 If you ever receive an unexpected or unrecognised response from us — stop sending traffic to iMoney immediately and contact support right away at techpg@imoneypg.com. Continuing to send transactions during such windows can lead to duplicate charges, stuck transactions, or reconciliation gaps. Pause first, escalate, then resume only after a written go-ahead from our tech team.


UAT Sign-off — Mandatory #

📝 Required before Production go-live

Every merchant must complete a UAT Sign-off with I Money Pay Private Limited. If you have not received the form, write to onboarding@imoneypay.in or techpg@imoneypg.com and request the UAT Sign-off Form. Signing it confirms that you have successfully configured all APIs and your error handling is implemented correctly.

Without a completed UAT Sign-off, I Money Pay Private Limited and its Tech Team will not be held responsible for any losses, mis-handled transactions, or integration defects on the merchant side.

This document is confidential and intended solely for the named merchant. iMoney reserves the right to update API specifications. Breaking changes will be communicated separately with a migration guide.