Payin P2C Seamless Integration
The Payin Seamless Request API allows you to initiate payment requests through payment Channel UPI. This API uses secure authentication with API Key validation, IP Whitelisting and Signature Verification for maximum security.
UPI is a set of APIs developed by NPCI to enable instant online payments. It simplifies immediate payments via mobile devices. Payments can be initiated by both the sender (payer) and the receiver (payee) and can be completed using virtual payment addresses, Aadhaar integration, mobile numbers, and more. The payer’s smartphone can securely capture credentials for these transactions.
Merchant On-boarding: The merchant must provide the following information for on-boarding in both the UAT and production environments:
IP Address (For dynamic IPs, please provide the range of IP addresses).
Merchant Callback URL to post the final transaction status from our system.
Once the merchant provides the required technical details, we will complete the necessary back-office configurations and provide a Merchant ID (MID/PID).
Let's see how it works:
Merchant sends a payment collection request through our API, they must include the
order_id,pid,amount,upi_id,name,email, andphonedetails.PAYMENT REQUEST : Upon receiving a request in the correct format, we will provide a UPI payment string, which is necessary for generating a QR code or creating a payment intent.
CALLBACK : After the customer completes the payment, you will receive callback data at the provided callback URL.
STATUS POLLING : To confirm or check the payment status, you can use the
polling_apiat any time to update your system about the payment.
AUTHENTICATION :
Required Authentication Headers
All requests must include the following authentication:
API Key: Your provided API key must be passed in X-Api-Key header
Signature: SHA256 signature for request validation
IP Whitelisting: Your server IP must be pre-registered in our system.
All V2 API requests must be authenticated by including a signature parameter in the JSON body.
The logic for creating this signature is identical for all V2 endpoints. Please follow the detailed instructions on our central guide:
➡️ V2 API Signature Generation
PAYMENT REQUEST :
Before proceeding with this section, please ensure you have reviewed the Basic Workflow of the system. This page outlines how to send a payment request effectively.
Important Note: All requests must originate from whitelisted IP addresses. Please verify that your IP is properly whitelisted before initiating any requests.
Payment Request
POST {baseURL}/api/v2/request.php
Headers
Content-Type
application/json
X-Api-Key
your-provided-api-key
Body
pid
string
Required
Your unique Partner ID provided during registration
amount
integer
Required
Your order amount
order_id
string
Required
Your unique order identifier
signature
string
Required
SHA256 signature for request validation (Refer this for signature generation logic Signature Generation)
ip
string
Required
Customer's IP address
name
string
Required
customer's name
email
string
Required
customer's email address
phone
string
Required
customer's phone number
latitude
string
Required
customer's location latitude
longitude
string
Required
customer's location longitude
customer_id
string
Required
customer's gameID
redirect_url
string
Required*
URL to redirect user the payment completion. *Required if not configured during registration
upi_id
string
Required*
customer's UPI ID *Required for intent flow
X-Api-Key: your-provided-api-key
Content-Type: application/json{
"pid": "your-partner-id",
"amount": 100,
"order_id": "order-123",
"signature": "generated-signature-hash",
"ip": "your-server-ip",
"name": "Customer Name",
"email": "[email protected]",
"phone": "9876543210",
"latitude": "28.7041",
"longitude": "77.1025",
"customer_id": "CUST001",
"upi_id": "customer@upi"
}Sample Response Body
{
"status": "success",
"ref_code": "unique-reference-code", //save this for future reference
"qr_code": "upi://pay?pa=merchant@upi&pn=MerchantName&am=100&cu=INR&tr=order-123",
"redirect_url": "https://payment-gateway.com/redirect",
"amount": 100,
"receiverVPA": "merchant@upi"
}{
"status": "error",
"message": "error message"
}Error Responses
Invalid Request data
Malformed JSON in request body
Check JSON syntax and structure
pid is required field
Missing partner ID
Include valid pid parameter
amount is required field
Missing amount parameter
Include valid amount parameter
order_id is required field
Missing order ID
Include valid order_id parameter
signature is required field
Missing signature for signature verification
Generate and include signature parameter
Invalid signature
Signature verification failed
Check signature generation logic
Unauthorized access
Missing API key in header
Include valid X-Api-key header
Invalid API key
API key doesn't match account
Use correct API key for your account
Invalid pid
Partner ID not found
Use correct partner ID
Please contact admin to Active Account...!
Account is inactive
Contact support for account activation
amount should be greater than: X
Amount below minimum limit
Use amount ≥ minimum limit
amount should be less than: X
Amount above maximum limit
use amount <= maximum limit
Duplicate order_id found
Order ID already exists
Use unique order_id for each request
Please contact admin to request whitelist your IP
IP not whitelisted
Contact support to whitelist your IP
Rate Limiting
Best Practices
Unique Order IDs: Always use unique order_id for each transaction
Amount validation: Validate amount limits before making requests
Error handling: Implement proper error handling for all possible responses
Secure storage: Keep your secret key and API key secure
HTTPS Only: Always use Https for Api requests
API key Security: Use your provided API key in X-Api-Key header for all requests
For Deep Link Integration
To integrate UPI (Unified Payments Interface) payments via deep links, you can use the UPI deep link ( qr_code link from response ) schemes provided by various payment apps like Google Pay, PhonePe, Paytm, etc. UPI allows for direct bank-to-bank transfers and is supported by multiple payment apps in India.
UPI Deep Link Format
The general format for UPI deep links is as follows:
upi://pay?pa=<merchant_vpa>&pn=<merchant_name>&mc=<merchant_code>&tid=<transaction_id>&tn=<transaction_note>&am=&cu=<currency_code>
Where:
pa: The merchant's UPI ID (VPA, e.g., merchant@upi).
pn: The merchant's name.
mc: The merchant code (optional).
tid: The unique transaction ID (optional, for tracking).
tn: Transaction note (optional, for description).
am: The amount to be paid (optional, can be left out if you want the user to enter the amount).
cu: Currency code, typically "INR" for Indian Rupees (optional).
Example for Google Pay and PhonePe (UPI Payment Deep Links)
Google Pay Deep Link (UPI)
To initiate a payment request via UPI through Google Pay:
gpay://upi/pay?pa=merchant@upi&pn=MerchantName&mc=123456&tid=TXN12345&tn=Payment for goods&am=100.00&cu=INR
Replace
upi://paywithgpay://upi/pay
PhonePe Deep Link (UPI)
PhonePe also supports UPI-based deep linking in a similar manner:
phonepe://pay?pa=merchant@upi&pn=MerchantName&tid=TXN12345&tn=Payment for goods&am=100.00&cu=INR
Replaceupi://pay with phonepe://pay
Paytm Deep Link (UPI)
Paytm uses a similar format to other apps:
paytmmp://pay?pa=merchant@upi&pn=MerchantName&tid=TXN12345&tn=Payment for goods&am=100.00&cu=INR
Replaceupi://pay with paytmmp://
General (UPI)
General UPI/BHIM uses a given format to all UPI apps:
upi://pay?pa=merchant@upi&pn=MerchantName&tid=TXN12345&tn=Payment for goods&am=100.00&cu=INR
How to Trigger the Deep Link
To trigger the UPI payment deep link, you can use the following methods depending on the platform you're working with:
Android
Use an Intent to open the UPI payment app (like Google Pay, PhonePe, etc.):
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("upi://pay?pa=merchant@upi&pn=MerchantName&mc=123456&tid=TXN12345&tn=Payment for goods&am=100.00&cu=INR"));
startActivity(intent);iOS
Use UIApplication to open the UPI link:
if let url = URL(string: "upi://pay?pa=merchant@upi&pn=MerchantName&mc=123456&tid=TXN12345&tn=Payment for goods&am=100.00&cu=INR") {
UIApplication.shared.open(url)
}Handling the Payment Result
Google Pay/PhonePe/Paytm will redirect back to your app if the user completes the payment. You can handle the result using a custom URL scheme or callback URL to process the payment status.
You need to set up an intent or listener in your app to capture the payment success/failure and take appropriate actions (e.g., updating the transaction status).
CALLBACK
We invoke your callback URL with callback data whenever there is a status change against the transaction.
Valid Transaction status are:
Pending
Approved
Late Approved
Amount Mismatch
User Timed out
Declined
Cancelled
Failed
The most famous transaction changes are (but not limited):
Pending=>Approved
Pending=>Amount Mismatch
Pending=>Declined
Pending=>User Timed Out
User Timed Out=>Late Approved
The callback landing page must be set up on your server at a secret path, but it should be publicly accessible from our whitelisted IP. (Ensure that it is only accessible from our server IP.)
In the POST body, the following properties will be provided in JSON format:
order_id
string
Your order id shared
requested_amount
string
requested amount
received_amount
string
received amount
bank_ref
string
transaction reference/bank reference/UTR if available
ref_code
string
unique code for the transaction
status
string
status of payment at this time
post_hash
string
signature post hash for security verification
Follow the steps to verify the integrity of received data:
Capture and Decode the Payload
Capture the raw JSON data from the POST body and decode it.
// Capture and decode the raw POST body
$data = file_get_contents("php://input");
$array = json_decode($data, true);// In a Node.js framework like Express, this is
// handled by middleware.
// app.use(express.json());
//
// Your data will be in req.body:
const array = req.body;# In a Python framework like Flask, this is
# handled by the request object.
#
# from flask import request
# array = request.get_json()Extract and Decode the
post_hash
The post_hash field in the JSON payload is a Base64-encoded string. You must Base64-decode it to get the raw binary data needed for decryption.
// Get the Base64 string from the array
$post_hash_base64 = $array['post_hash'];
// Decode it to get the raw binary ciphertext
$ivHashCiphertext = base64_decode($post_hash_base64);// Get the Base64 string from the array
const postHashBase64 = array.post_hash;
// Decode it to get the raw binary buffer
const ivHashCiphertext = Buffer.from(postHashBase64, 'base64');import base64
# Get the Base64 string from the array (dict)
post_hash_base64 = array['post_hash']
# Decode it to get the raw binary data
ivHashCiphertext = base64.b64decode(post_hash_base64)Decrypt the Binary Data to get the Remote Hash
Pass the raw binary data (not the Base64 string) to the decrypt function along with your secret key.
//$secret_key is your provided SECRET KEY
$remote_hash = decrypt($ivHashCiphertext, $secret_key);// const secret_key = "YOUR_SECRET_KEY";
const remote_hash = decrypt(ivHashCiphertext, secret_key);# secret_key = "YOUR_SECRET_KEY"
remote_hash = decrypt(ivHashCiphertext, secret_key)The decrypt function for your language is provided in the reference section below.
function decrypt($ivHashCiphertext, $password)
{
$method = "AES-256-CBC";
$iv = substr($ivHashCiphertext, 0, 16);
$hash = substr($ivHashCiphertext, 16, 32);
$ciphertext = substr($ivHashCiphertext, 48);
$key = hash('sha256', $password, true);
if (!hash_equals(hash_hmac('sha256', $ciphertext . $iv, $key, true),$hash))
return null;
return openssl_decrypt($ciphertext, $method, $key,
OPENSSL_RAW_DATA, $iv);
}const crypto = require('crypto');
/**
* @param {Buffer} ivHashCiphertext - The raw binary data (Buffer) after Base64-decoding.
* @param {string} password - Your secret key.
* @returns {string|null} The decrypted plaintext or null on failure.
*/
function decrypt(ivHashCiphertext, password) {
const method = 'aes-256-cbc';
// Extract the initialization vector (first 16 bytes)
const iv = ivHashCiphertext.slice(0, 16);
// Extract the hash (next 32 bytes)
const hash = ivHashCiphertext.slice(16, 48);
// Extract the ciphertext (remaining bytes)
const ciphertext = ivHashCiphertext.slice(48);
// Generate the key using SHA-256 hash of the password
const key = crypto.createHash('sha256').update(password, 'utf8').digest();
// Compute HMAC-SHA256 of (ciphertext || iv) using the key
const hmac = crypto.createHmac('sha256', key)
.update(Buffer.concat([ciphertext, iv]))
.digest();
// Compare the computed HMAC with the extracted hash
if (!crypto.timingSafeEqual(hmac, hash)) {
return null; // Hashes do not match; return null
}
try {
// Decrypt the ciphertext using AES-256-CBC
const decipher = crypto.createDecipheriv(method, key, iv);
const plaintext = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
return plaintext.toString('utf8'); // Return the decrypted text as a UTF-8 string
} catch (err) {
// Decryption failed; return null
return null;
}
}import hashlib
import hmac
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
def decrypt(ivHashCiphertext, password):
"""
Decrypts the iv/hash/ciphertext blob.
:param ivHashCiphertext: The raw binary data (after base64-decoding).
:param password: Your secret key string.
:return: The decrypted plaintext string, or None on failure.
"""
try:
# 1. Extract the components
iv = ivHashCiphertext[:16]
hash_val = ivHashCiphertext[16:48]
ciphertext = ivHashCiphertext[48:]
# 2. Generate the key
key = hashlib.sha256(password.encode('utf-8')).digest()
# 3. Verify the HMAC
computed_hash = hmac.new(key, ciphertext + iv, hashlib.sha256).digest()
if not hmac.compare_digest(computed_hash, hash_val):
return None
# 4. Decrypt the ciphertext
backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
decryptor = cipher.decryptor()
# Decrypt and unpad
# Using PKCS7 unpadder explicitly is more robust
decrypted_padded_text = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
decrypted_text = unpadder.update(decrypted_padded_text) + unpadder.finalize()
return decrypted_text.decode('utf-8')
except Exception as e:
# Catch any error (e.g., bad padding, bad key) and return None
# print(f"Decryption failed: {e}") # Optional: for debugging
return NoneCompute the Local Hash
Compute the local hash using the MD5 128-bit hashing algorithm. Use the order_id, received_amount, and status received in the callback array for computing the local hash.
Note: The received_amount in the callback payload is always sent as an integer string (e.g., "100", "0").
// Get the values from the same callback $array
$order_id = $array['order_id'];
$received_amount = $array['received_amount'];
$status = $array['status'];
$local_hash = md5($order_id . $received_amount . $status . $secret_key);const crypto = require('crypto');
// Get the values from the same callback array
const order_id = array.order_id;
const received_amount = array.received_amount;
const status = array.status;
const local_hash = crypto
.createHash('md5')
.update(order_id + received_amount + status + secret_key)
.digest('hex');import hashlib
# Get the values from the same callback array (dict)
order_id = array['order_id']
received_amount = array['received_amount']
status = array['status']
local_hash = hashlib.md5((order_id + received_amount + status + secret_key).encode('utf-8')).hexdigest()Verify Hash
Compare the decrypted $remote_hash from the request and the computed $local_hash.
if ($remote_hash !== null && hash_equals($local_hash, $remote_hash)) {
// Mark the transaction as success & process the order
$hash_status = "Hash Matched";
} else {
// Verification failed
$hash_status = "Hash Mismatch";
}let hash_status = "Hash Mismatch";
try {
// Both remote_hash (decrypted) and local_hash are hex strings.
// We must use 'utf8' buffers for timingSafeEqual.
const localHashBuffer = Buffer.from(local_hash, 'utf8');
const remoteHashBuffer = Buffer.from(remote_hash, 'utf8');
if (localHashBuffer.length === remoteHashBuffer.length &&
crypto.timingSafeEqual(localHashBuffer, remoteHashBuffer)) {
hash_status = "Hash Matched";
}
} catch (e) {
// hash_status remains "Hash Mismatch"
}import hmac
# Use hmac.compare_digest for secure comparison
if remote_hash and hmac.compare_digest(local_hash, remote_hash):
hash_status = "Hash Matched"
else:
hash_status = "Hash Mismatch"Acknowledge the Payment Gateway
To confirm you have received the callback and to prevent our gateway from sending retries, you must do two things:
Respond with an HTTP 200 OK status code.
Respond with a JSON body containing the key
"acknowledge"set to the string"yes".
Our system will check for both the 200 status and the {"acknowledge": "yes"} in the response body. If either is missing, we will assume the callback failed and will attempt to send it again.
// --- This is the required acknowledgment ---
// 1. Set the HTTP 200 OK status code
http_response_code(200);
// 2. Prepare the required JSON response body
$response_data = [
'acknowledge' => 'yes',
'hash_status' => $hash_status // You can include this for your own logs
];
// 3. Send the response
header('Content-Type: application/json; charset=utf-8');
echo json_encode($response_data);
exit;// In a Node.js framework like Express,
// this sends both the 200 status and the JSON body.
res.status(200).json({
acknowledge: 'yes',
hash_status: hash_status
});# In a Python framework like Flask
from flask import jsonify
response_data = {
'acknowledge': 'yes',
'hash_status': hash_status
}
# This returns a 200 status and the JSON body
return jsonify(response_data), 200STATUS POLLING :
The payment Status Polling API allows you to retrieve real-time status information for payment transactions. This endpoint provides comprehensive transaction details including payment status, amounts etc.
Note : All requests must include proper authentication headers and IP whitelisting may be required for production environments.
POST baseurl/api/v2/status_polling.php
Authentication
This API required the following authentication mechanism:
Required Headers
Content-Type
string
Yes
Must be application/json
X-Api-Key
string
Yes
Your provided API key for authentication
IP Whitelisting
Your server IP address must be whitelisted in production environments. Contact your administration to ensure your IP is properly configured.
Request Parameters
pid
string
Yes
Merchant ID/PID
ref_code
string
Yes
Unique ref_code which is generated in payment request
post_hash
string
Yes
The Base64-encoded encrypted hash. (See steps below).
Steps to generate post_hash :
Generate the Request
post_hash
1.1 Create Plaintext Hash: Concatenate the ref_code, pid, and your secret_key, then create an MD5 hash.
$ref_code = "YOUR_REF_CODE";
$pid = "YOUR_PID";
$secret_key = "YOUR_SECRET_KEY";
$local_hash = md5($ref_code . $pid . $secret_key);const crypto = require('crypto');
const ref_code = "YOUR_REF_CODE";
const pid = "YOUR_PID";
const secret_key = "YOUR_SECRET_KEY";
const local_hash = crypto
.createHash('md5')
.update(ref_code + pid + secret_key)
.digest('hex');
import hashlib
ref_code = "YOUR_REF_CODE"
pid = "YOUR_PID"
secret_key = "YOUR_SECRET_KEY"
local_hash = hashlib.md5((ref_code + pid + secret_key).encode('utf-8')).hexdigest()1.2 Encrypt the Hash: Encrypt the $local_hash using the encrypt function shown below.
$encrypted_hash = encrypt($local_hash, $secret_key);function encrypt($plaintext, $password)
{
$method = "AES-256-CBC";
$key = hash('sha256', $password, true);
$iv = openssl_random_pseudo_bytes(16);
$ciphertext = openssl_encrypt($plaintext, $method, $key,
OPENSSL_RAW_DATA, $iv);
$hash = hash_hmac('sha256', $ciphertext . $iv, $key, true);
return $iv . $hash . $ciphertext;
}import hashlib
import hmac
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding as crypto_padding
def encrypt(plaintext, password):
key = hashlib.sha256(password.encode('utf-8')).digest()
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext_bytes = plaintext.encode('utf-8')
padded_plaintext = pad(plaintext_bytes, AES.block_size)
ciphertext = cipher.encrypt(padded_plaintext)
hash_value = hmac.new(key, ciphertext + iv, hashlib.sha256).digest()
return iv + hash_value + ciphertextconst crypto = require('crypto');
function encrypt(plaintext, password) {
const method = 'aes-256-cbc';
const key = crypto.createHash('sha256').update(password, 'utf8').digest();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(method, key, iv);
let ciphertext = cipher.update(plaintext, 'utf8');
ciphertext = Buffer.concat([ciphertext, cipher.final()]);
const hmac = crypto.createHmac('sha256', key);
hmac.update(Buffer.concat([ciphertext, iv]));
const hash = hmac.digest();
// Return the raw binary Buffer
return Buffer.concat([iv, hash, ciphertext]);
}1.3 Base64 Encode: Base64-encode the raw binary output from the encrypt function. This final string is your post_hash.
$post_hash = base64_encode($encrypted_hash);Send the POST Request
Send a POST request containing pid, ref_code, and post_hash as a JSON body , and you will receive a response after validating the data.
<?php
// Your transaction data
$pid = "YOUR_PID";
$ref_code = "YOUR_REF_CODE";
$secret_key = "YOUR_SECRET_KEY";
// --- Step 1: Generate Hash ---
$local_hash = md5($ref_code . $pid . $secret_key);
$encrypted_hash = encrypt($local_hash, $secret_key);
$post_hash = base64_encode($encrypted_hash);
// --- Step 2: Send Request ---
$api_url = "https://<domain>/api/v2/status_polling.php";
$data = [
'pid' => $pid,
'ref_code' => $ref_code,
'post_hash' => $post_hash
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $api_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-Api-Key: YOUR_API_KEY_HERE'
]);
$server_output = curl_exec($ch);
curl_close($ch);
?>
Processing the API Response
The API will respond with a JSON object. If the request is successful and the ref_code is found, it will return the transaction details. If it fails (e.g., bad hash, ref_code not found), it will return an error message.
Success Response Parameters
A successful response will contain the following parameters as JSON body:
order_id
String
Your order ID.
ref_code
String
The unique ref_code for this transaction.
upi_id
String
The UPI ID the payment was made to.
requested_amount
Number
The amount originally requested for the transaction.
received_amount
Number
The final amount confirmed as received.
bank_ref
String
The bank's UTR / reference number, if available.
sender_upi
String
The customer's UPI ID from which the payment was received.
webhook_acknowledged
String
"1" if our server has sent the callback, "0" otherwise.
status
String
The current status of the transaction (e.g., Approved, Pending, Declined, etc).
post_hash
String
A Base64-encoded encrypted hash to verify the integrity of this response.
Error Response
If your request fails, the server will respond with an appropriate HTTP status code and a JSON body like the following.
{
"status": "error",
"message": "error message"
}Common Error Responses
400
No input data received
The raw POST body was empty.
400
Invalid JSON format in request body
The data sent was not valid JSON.
400
Missing required parameters (...)
Your JSON body is missing pid, ref_code, or post_hash.
400
Invalid PID
The pid sent does not exist in our system.
400
Reference code not found.
The ref_code sent does not exist in our system.
400
Invalid hash
The signature verification failed. The hashes do not match.
400
Invalid base64 encoding in post_hash
The post_hash string was not valid Base64.
401
X-Api-Key header is required
The X-Api-Key was not found in the request headers.
401
Invalid API key
The provided X-Api-Key is incorrect.
403
Please contact admin to whitelist your IP: ...
Your server's IP is not in the whitelist for this account.
429
[Dynamic rate limit message]
You have exceeded the allowed request rate.
500
Internal Server Error
A server-side error occurred. Please contact support.
Verify the Response
post_hash
Before trusting any data from the response, you must verify its post_hash to ensure the data is from us and has not been tampered with. This verification logic is identical to the logic used for verifying a callback.
You do not need to send an "Acknowledge" response for a status poll.
4.1 Compute the Local Hash
From the JSON response, get the order_id, received_amount, and status. Concatenate them with your secret_key and create an MD5 hash.
// $response_data is the decoded JSON response from /api/status_polling.php
$order_id = $response_data['order_id'];
$received_amount = $response_data['received_amount'];
$status = $response_data['status'];
$local_hash = md5($order_id . $received_amount . $status . $secret_key);const crypto = require('crypto');
// responseData is the parsed JSON response
const order_id = responseData.order_id;
const received_amount = responseData.received_amount;
const status = responseData.status;
const local_hash = crypto
.createHash('md5')
.update(order_id.toString() + received_amount.toString() + status.toString() + secret_key)
.digest('hex');import hashlib
# response_data is the parsed JSON response (a dict)
order_id = response_data['order_id']
received_amount = "%g" % response_data['received_amount']
status = response_data['status']
local_hash = hashlib.md5((order_id + received_amount + status + secret_key).encode('utf-8')).hexdigest()4.2 Decrypt the Remote Hash
Get the post_hash string from the JSON response. Base64-decode it, then pass the raw binary data to the decrypt function.
// Get the Base64 string from the response
$post_hash_base64 = $response_data['post_hash'];
// Decode it to get the raw binary ciphertext
$ivHashCiphertext = base64_decode($post_hash_base64);
// Decrypt using the function from the reference section below
$remote_hash = decrypt($ivHashCiphertext, $secret_key);const postHashBase64 = responseData.post_hash;
// Decode it to get the raw binary buffer
const ivHashCiphertext = Buffer.from(postHashBase64, 'base64');
// Decrypt using the function from the reference section below
const remote_hash = decrypt(ivHashCiphertext, secret_key);import base64
post_hash_base64 = response_data['post_hash']
# Decode it to get the raw binary data
ivHashCiphertext = base64.b64decode(post_hash_base64)
# Decrypt using the function from the reference section below
remote_hash = decrypt(ivHashCiphertext, secret_key)The decrypt function for your language is provided in the reference section below.
function decrypt($ivHashCiphertext, $password)
{
$method = "AES-256-CBC";
$iv = substr($ivHashCiphertext, 0, 16);
$hash = substr($ivHashCiphertext, 16, 32);
$ciphertext = substr($ivHashCiphertext, 48);
$key = hash('sha256', $password, true);
if (!hash_equals(hash_hmac('sha256', $ciphertext . $iv, $key, true),$hash))
return null;
return openssl_decrypt($ciphertext, $method, $key,
OPENSSL_RAW_DATA, $iv);
}const crypto = require('crypto');
/**
* @param {Buffer} ivHashCiphertext - The raw binary data (Buffer) after Base64-decoding.
* @param {string} password - Your secret key.
* @returns {string|null} The decrypted plaintext or null on failure.
*/
function decrypt(ivHashCiphertext, password) {
const method = 'aes-256-cbc';
// Extract the initialization vector (first 16 bytes)
const iv = ivHashCiphertext.slice(0, 16);
// Extract the hash (next 32 bytes)
const hash = ivHashCiphertext.slice(16, 48);
// Extract the ciphertext (remaining bytes)
const ciphertext = ivHashCiphertext.slice(48);
// Generate the key using SHA-256 hash of the password
const key = crypto.createHash('sha256').update(password, 'utf8').digest();
// Compute HMAC-SHA256 of (ciphertext || iv) using the key
const hmac = crypto.createHmac('sha256', key)
.update(Buffer.concat([ciphertext, iv]))
.digest();
// Compare the computed HMAC with the extracted hash
if (!crypto.timingSafeEqual(hmac, hash)) {
return null; // Hashes do not match; return null
}
try {
// Decrypt the ciphertext using AES-256-CBC
const decipher = crypto.createDecipheriv(method, key, iv);
const plaintext = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
return plaintext.toString('utf8'); // Return the decrypted text as a UTF-8 string
} catch (err) {
// Decryption failed; return null
return null;
}
}import hashlib
import hmac
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
def decrypt(ivHashCiphertext, password):
"""
Decrypts the iv/hash/ciphertext blob.
:param ivHashCiphertext: The raw binary data (after base64-decoding).
:param password: Your secret key string.
:return: The decrypted plaintext string, or None on failure.
"""
try:
# 1. Extract the components
iv = ivHashCiphertext[:16]
hash_val = ivHashCiphertext[16:48]
ciphertext = ivHashCiphertext[48:]
# 2. Generate the key
key = hashlib.sha256(password.encode('utf-8')).digest()
# 3. Verify the HMAC
computed_hash = hmac.new(key, ciphertext + iv, hashlib.sha256).digest()
if not hmac.compare_digest(computed_hash, hash_val):
return None
# 4. Decrypt the ciphertext
backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
decryptor = cipher.decryptor()
# Decrypt and unpad
# Using PKCS7 unpadder explicitly is more robust
decrypted_padded_text = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
decrypted_text = unpadder.update(decrypted_padded_text) + unpadder.finalize()
return decrypted_text.decode('utf-8')
except Exception as e:
# Catch any error (e.g., bad padding, bad key) and return None
# print(f"Decryption failed: {e}") # Optional: for debugging
return None4.3 Compare the Hashes
Securely compare the local_hash you just computed with the remote_hash you decrypted. If they match, you can trust the data.
if ($remote_hash !== null && hash_equals($local_hash, $remote_hash)) {
// --- SUCCESS: Data is verified ---
// You can now trust the data and update your database.
// echo "Status: " . $response_data['status'];
} else {
// --- FAILURE: Hash mismatch! ---
// Do NOT trust this data.
}let isVerified = false;
if (remote_hash) {
// Use crypto.timingSafeEqual for secure comparison
try {
const localHashBuffer = Buffer.from(local_hash, 'utf8');
const remoteHashBuffer = Buffer.from(remote_hash, 'utf8');
if (localHashBuffer.length === remoteHashBuffer.length) {
isVerified = crypto.timingSafeEqual(localHashBuffer, remoteHashBuffer);
}
} catch (e) {
// Error during buffer creation (e.g., bad hash)
isVerified = false;
}
}
if (isVerified) {
// --- SUCCESS: Data is verified ---
// console.log("Status:", responseData.status);
} else {
// --- FAILURE: Hash mismatch! ---
}import hmac
isVerified = False
if remote_hash:
# Use hmac.compare_digest for secure comparison
isVerified = hmac.compare_digest(local_hash, remote_hash)
if isVerified:
# --- SUCCESS: Data is verified ---
# print(f"Status: {response_data['status']}")
pass
else:
# --- FAILURE: Hash mismatch! ---
passRate Limiting
This API endpoint implements rate limiting to prevent abuse. If you exceed the allowed request rate, you will receive an error response.
Rate Limit Response: When rate limit is exceeded, you'll receive an error message with details about the limitation.
Security Considerations
Hash Verification: Always verify the response hash to ensure data integrity
HTTPS only: Use HTTPS for all API considerations
API key Protection: Keep your provided API keys secure and never expose them in client-side code
IP whitelisting: Ensure your server IP is properly whitelisted
Rate Limiting: Implement proper retry logic with exponential backoff
TRANSACTION STATUS :
Amount Mismatch : We received money but customer paid different money than the requested money
Approved : We received money same value as requested
Late Approved : We recieved money but it happened late while doing reconcilation from bank side
Declined : The transaction declined due to security reasons
Failed : The payment failed from bank side
Cancelled : This status for NonSeamless when customer cancel the payment from the screen
User Timed Out: The user did not complete the payment within the session period.
COMPLAINT
We have a dedicated Complaint Section where merchants can manage transaction-related complaints. Through this section, merchants can submit complaints with all necessary details and optional evidence. Upon submission, a unique complaint reference ID is generated, allowing merchants to track the complaint’s status and receive real-time updates via the status-check API. This ensures a smooth, secure, and efficient process for resolving any transaction issues.
RECONCILIATION
This API endpoint allows authorized users to retrieve payment transactions based on a specific pid (Partner ID) and date. The API performs authentication using a token and signature verification to ensure secure communication.
Last updated