Payin P2C Seamless Wallet Integration
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 Onboarding: The merchant must provide the following information for onboarding 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 need to provide the customer's
wallet_type
,order_id
,pid
,amount
,name
,email
, andphone
details.PAYMENT REQUEST : Upon receiving the request in the correct format, we will share a payment
redirect_url
that redirects the customer to the selected wallet's payment screen.CALLBACK : After the payment is verified, you will receive callback data at the provided callback URL.
STATUS POLLING : You can confirm or check the payment status by calling the
polling_api
at any time and update your system accordingly.
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
https://<domain>/api/request.php
Merchant makes a payment request.
Headers
Content-Type
application/json
Body
pid
string
provided MID/PID
order_id
string
unique order id
amount
integer
amount
phone
integer
Customer phone number
email
string
Customer email address
name
string
Customer contact name
{
"pid": "0951272386617",
"order_id": "MXmzie9932idwq3",
"amount": "43",
"upi_id": "testpaybank123@upi",
"wallet_type": "bKash"
"phone":"9895000000",
"name":"exampleName",
"email":"example@example.com"
}
Sample Response Body
{
"ref_code": "491836c1346f84516af3179e6b14e54f6914ab3e4eb59a62b6d2913781152c6d",
"wallet_id": "",
"wallet_type": "bKash",
"amount": 43,
"status": "success",
"wallet_url": "https://payment.bkash.com/?paymentId=TR00115SWdn0W1727439860411&hash=2vsy-Gcek8U0OZ-j!DqF2VJs6Oux4tJyNrbM).o!5DTL0iZZs*fgAz4OrJGAjymdY0z7t7SKte1App*J4r*YpgSeaWoI6szVTpLK1727439860411&mode=0011&apiVersion=v1.2.0-beta/"
}
CALLBACK
The callback is invoked whenever there is any status change in the transaction.
Valid Transaction status are:
Approved
Declined
Late Approved
User Timed Out
No Matching Payment for UTR
Pending
The most famous transaction changes are (but not limited):
Pending=>Approved
Pending=>Declined
Pending=>User Timed Out
User Timed Out=>Late Approved
Pending=>No matching payment for UTR
No Matching Payment for UTR => Late Approved
The callback/Webhook landing page has to be available on your server at some secret path but it should be publicly available from our whitelisted IP.
Request Fields : secret_key = given secret key;
In the POST body, you will get the following properties in JSON:
order_id
string
Your order id shared
requested_amount
int
requested amount
received_amount
int
received amount
bank_ref
string
bank reference/UTR if available
ref_code
string
unique code for the transaction
status
string
status of payment at this time
post_hash
string
post hash for security verification
refund_info
string
if any refund data available
Refund Details Field (if refund initiated)
refunded_upi
string
to which upi refund has made
refund_amount
string
refund amount
refund_initiated_time
string
time of refund
refund_status
string
status of refund
refund_notes
string
any notes available about refund
Follow the steps to verify the integrity of received data:
base64_decode post_hash:
Capture JSON data from the POST body.
JSON decode the data to an array or object.
Extract the
post_hash
from the decoded data.For encrypted
post_hash
base64_decode thepost_hash
.
$data = file_get_contents("php://input");
$json = json_decode($data, true);
$encrypted_hash=base64_decode($json['post_hash']);
Decrypt hash
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);
}
$remote_hash=decrypt($encrypted_hash,$secret_key);
Compute the local hash using the MD5 128-bit hashing algorithm. Generate the hash locally.
$local_hash = md5($order_id.$received_amount.$status.$secret_key);
Verify hash (Compare hash given at request and local hash)t
if ($remote_hash == $local_hash)
{
// consider received amount to update
// Mark the transaction as success & process the order
// You can write code process the order herer
// Update your db with payment success
$hash_status = "Hash Matched";
}
else
{
// Verification failed
$hash_status = "Hash Mismatch";
}
Acknowledge Back server (You should Acknowledge our payment gateway that you saved the status of payment, otherwise you will get multiple acknowledge because we have a retry mechanism for failed webhooks)
$data['hash_status']=$hash_status;// 'Hash Matched' or 'Hash Mismatch'
$data['acknowledge']=$acknowledge;// 'yes' or 'no'
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data);// output as a json file
Definition of Payment Status:
Pending: User session in active waiting to finish payment
Approved: A Successful Payment
Declined: Payment is declined by our system
User Timed Out: User didn’t finished payment within the session period
No Matching Payment for UTR: system waited till timeout but no payment/matching UTR received against the payment.
STATUS POLLING :
POST
https://<domain>/api/status_polling.php
This API is for polling the status for a particular transaction.
Headers
Content-Type
application/json
Body
secret_key
string
given secret key
url_of_polling_api
string
you will get it
pid
string
Merchant ID/PID
ref_code
string
unique ref_code which is generated in payment request
post_hash
string
post hash for signature verification
Steps to generate post_hash :
Create a hash using md5 algorithm by appending values of ref_code, pid, secret_key
$local_hash = md5($ref_code . $pid . $row['secret_key']);
NodeJS Example:
const local_hash = crypto.createHash('md5').update(ref_code + pid + secret_key).digest('hex');
const encodedStr=encrypt(local_hash, secret_key).toString('base64');
Encrypt hash (You need to encrypt the hash using the secret key)
$encrypted_hash=encrypt($local_hash, $row['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;
}
base64_encode encrypted_hash for safe delivery over the network.
//Compute the payment hash locally
$encoded_hash=base64_encode($encrypted_hash);
Send a post request to the given URL
Send a post request that contains pid,ref_code and post_hash(as jSON post body) to url_of_polling_api and you will get a response after validating the data.
<?php
//A very simple PHP example that sends a HTTP POST to a remote site
$data['pid']=pid;
$data['ref_code']=ref_code;
$data['post_hash']=post_hash;
$ch = curl_init();
$url=you will get api url in the call
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_POST, 1); // In real life you should use something like:
curl_setopt($ch, CURLOPT_POSTFIELDS,json_encode($data,true));
// Receive server response …
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$server_output = curl_exec($ch);
curl_close($ch);
if($server_output)
{
// You can follow step 5 to process response
}
?>
Process Response (You will get a JSON response)
order_id
string
Order id
requested_amount
int
requested amount
recieved_amount
int
recieved amount
bank_ref
string
bank reference id or UTR if available
webhook_acknowledged
string
web acknowledge status 0 or 1
status
string
payment approval status either Approved,Declined,Pending
post_hash
string
payload verification encrypted hash
refund_info
string
information about refund request
Refund Information details
refunded_upi
string
Order id
refund_amount
string
requested amount
refund_initiated_time
string
recieved amount
refund_completed_time
string
bank reference id or UTR if available
refund_status
string
web acknowledge status 0 or 1
refund_notes
string
payment approval status either Approved,Declined,Pending
Status API Response Process
$data = file_get_contents("php://input");
$row1=json_decode($data, true);
$row1['order_id'];
$row1['upi_id'];
$row1['amount'];
$row1['webhook_acknowledged'];
$row1['status'];
$row1['post_hash'];// decode post hash
$encrypted_hash=base64_decode($row1['post_hash']); // decrypt encrypted hash
$remote_hash = decrypt($encrypted_hash,$row['secret_key']);
$local_hash = md5($order_id . $data['amount']. $data['status'].$row['secret_key']); // generate local hash
Verify Response
#PHP Example if $local_hash equal to $remote_hash then the data is verified:
if($remote_hash==$local_hash)
{ // validated status }
else { // invalid status }
Example: Request
{
"ref_code": "asas63e1fe596ed8",
"pid":"5345f345345",
"post_hash":"kvDFE0f/iUuVQ4bZKufsjnUNxs4CN8Hqn6yvApqmoZQZ+h+HUidxTRvv6UxKVBnYwyNA3GamOwGFrtLslvQf20GOcFUz73wqHkvMSZdmUIXRKdbTOWm8YRzsxxXAJqpr",
"amount":100
}
Example: status polling response
{
"order_id": "63e1fe596ed8",
"amount": "2000",
"webhook_acknowledged": "0",
"status": "Approved",
"post_hash": "N1xxowl7aIamQQYCzfJ7lX7t8Q9GJUzn1XAQa01XHvXF4Qfym1drCTUAk4uqw1AeWFB6OEqG5ttJy9PsVunu0rBaDrIChF7m8Qhp1Rp3GyO74d9E3+QxGl9sdQDsdf55opo",
"refund_info": []
}
Python Example for encrypt, decrypt, hashing functions:
import base64
import os
# pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import hashlib
import hmac
def hash_hmac(algorithm, data, key, raw_output=False):
if isinstance(data, str):
data = data.encode('utf-8')
if isinstance(key, str):
key = key.encode('utf-8')
hmac_hash = hmac.new(key, data, getattr(hashlib, algorithm.lower()))
if raw_output:
return hmac_hash.digest()
else:
return hmac_hash.hexdigest()
def encrypt(plaintext, password):
key = hashlib.sha256(password.encode()).digest()
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext.encode(), AES.block_size))
hash_value = hash_hmac("SHA256",ciphertext + iv,key,True)
return iv + hash_value + ciphertext
def decrypt(ivHashCiphertext, password):
# Segregating IV,Hash,Cipher
iv = ivHashCiphertext[:16]
hash_val = ivHashCiphertext[16:48]
ciphertext = ivHashCiphertext[48:]
key = hashlib.sha256(password.encode()).digest()
if not hmac.compare_digest( hash_hmac("SHA256",ciphertext + iv,key,True), hash_val
):
return None
backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
decryptor = cipher.decryptor()
decrypted_text = decryptor.update(ciphertext) + decryptor.finalize()
return decrypted_text.decode()
# Example usage:
plaintext = "Hello, worlds!"
password = "mysecretpassword"
encrypted_data = encrypt(plaintext, password)
# Encode cipher for transport
encoded_cipher=base64.b64encode(encrypted_data)
# decode cipher for decrypt
iv_hash_ciphertext=base64.b64decode(encoded_cipher)
decrypted_data = decrypt(iv_hash_ciphertext, password)
print(decrypted_data)
Node JS Decrypt function
const crypto = require('crypto');
function decrypt(ivHashCiphertext, password) {
// If ivHashCiphertext is a string, assume it's base64 encoded
if (typeof ivHashCiphertext === 'string') {
ivHashCiphertext = Buffer.from(ivHashCiphertext, 'base64');
}
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;
}
}
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