Payin P2C Seamless 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 On-boarding: The merchant must provide the following information for on-boarding in both the UAT and production environments:

  1. IP Address (For dynamic IPs, please provide the range of IP addresses).

  2. 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:

  1. Merchant sends a payment collection request through our API, they must include the order_id, pid, amount, upi_id, name, email, and phone details.

  2. 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.

  3. CALLBACK : After the customer completes the payment, you will receive callback data at the provided callback URL.

  4. STATUS POLLING : To confirm or check the payment status, you can use the polling_api at any time to update your system about the payment.

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

Name
Value

Content-Type

application/json

Body

Name
Type
Description
Mandatory

pid

string

provided MID/PID

Yes

order_id

string

unique order id

Yes

amount

int

requested amount

Yes

upi_id

string

customer's upi id

No

name

string

customer's name

Yes

email

string

customer's email

Yes

phone

string

customer's phone

Yes

{
    "pid":"0951272386617",
    "order_id":"jdherfecuh0",
    "amount": 87,
    "upi_id":"[email protected]",
    "name":"john",
    "email":"[email protected]",
    "phone":"8974554630"
}

Sample Response Body

{
    "ref_code": "18855d71b83ce84c13b0f924b4efbb40a4b0bbd4cda7f9747906eb49dc8b3fcb",
    "qr_code": "upi://pay?pa=mahekpg@testupi&pn=5fk2p&am=87&tn=5fk2p",
    "status": "success",
    "redirect_url": ""
    "amount": 87,
    "receiverVPA": "mahekpg@testupi"
}

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.

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).

  1. 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

  • Replaceupi://pay with gpay://upi/pay

  1. 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

  1. 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://

  1. 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

To trigger the UPI payment deep link, you can use the following methods depending on the platform you're working with:

  1. 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);
  1. 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

  1. 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.

  2. 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:

  1. Approved

  2. Declined

  3. Late Approved

  4. Pending

  5. User Timed Out

  6. Cancelled

  7. Failed

  8. Amount Mismatch

The most famous transaction changes are (but not limited):

  1. Pending=>Approved

  2. Pending=>Declined

  3. Pending=>User Timed Out

  4. 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:

Name
Type
Description

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

Note:

please consider received_amount for final transaction processing

Follow the steps to verify the integrity of received data:

  1. Capture and Decode the Payload

Capture the raw JSON data from the POST body and decode it.

$data = file_get_contents("php://input");
$array = json_decode($data,  true);
  1. 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);
  1. 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);

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);                               
}

  1. Compute 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.

// 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);
  1. Verify Hash

Compare the decrypted $remote_hash from the request and the computed $local_hash.

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 here  
    // Update your db with payment success  
    $hash_status = "Hash Matched";    
}  
else  
{  
    // Verification failed       
    $hash_status = "Hash Mismatch";  
}
  1. 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;

STATUS POLLING :

POST https://<domain>/api/status_polling.php

This API is used to poll the status of a particular transaction.

Headers

Name
Value

Content-Type

application/json

Body

Name
Type
Description
Mandatory

pid

string

Provided Merchant ID/PID

Yes

ref_code

string

unique ref_code which is generated in payment request

Yes

post_hash

string

The Base64-encoded encrypted hash. (See steps below).

Yes

Steps to generate post_hash :

  1. 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);

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;
}

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);
  1. 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 (cURL) Example:
<?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/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']);

$server_output = curl_exec($ch);
curl_close($ch);

?>
  1. 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

An error response will contain an error key.

{
    "error": "error message"
}
  1. 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);

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);

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);                               
}

4.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.
}

TRANSACTION STATUS :

  1. Amount Mismatch : We received money but customer paid different money than the requested money

  2. Approved : We received money same value as requested

  3. Late Approved : We recieved money but it happened late while doing reconcilation from bank side

  4. Declined : The transaction declined due to security reasons

  5. Failed : The payment failed from bank side

  6. Cancelled : This status for NonSeamess when customer cancel the payment from the screen

  7. 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.

Complaint

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.

Reconciliation

Last updated