# Payout Seamless UPI Integration

For onboarding in both the UAT and production environments, the merchant needs to provide the following information:

**Technical Information Required:**

1. **IP Address:** (For dynamic IPs, please provide a range of IP addresses.)
2. **Callback URL/WebHook URL:** This is used to notify your server of any changes in transaction status from our back office.

Once the merchant has provided all the required technical details, we will complete the necessary configuration in our back office and provide the Merchant ID (MID/PID) along with login credentials.

### <mark style="color:orange;">Let's see how it works:</mark>&#x20;

1. The merchant will send a payout request through our API. Along with that, the merchant has to send the customer's UPI id, account\_holder, the amount, payment\_mode as upi.
2. Once we receive the request in the correct format, we will share a payment reference string (ref\_code) that is necessary for any future references or actions related to the transaction.
3. After the payout is processed, whether successful or failed, we will send callback data to the provided callback URL (WebHook URL) with the details of the transaction.&#x20;
4. To confirm the status of the payout, the merchant can use the polling API to check the current status and update their system accordingly with the payment outcome. (Status Polling)

{% hint style="info" %}
**Api Key**

`Your provided API key must be passed in X-Api-Key header for all requests.`

**Security**

`All requests are secured with signature verification and IP whitelisting.`
{% endhint %}

## <mark style="color:orange;">AUTHENTICATION :</mark>

#### 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**](https://doc.payatom.in/api-integration/v2/signature-generation)

## <mark style="color:orange;">PAYOUT REQUEST :</mark>

Before proceeding with this section of the document, the developer must review the basic workflow. This page explains how to submit a payment request.

**Note:** All requests must originate from whitelisted IP addresses.\
(Please ensure that your IP is whitelisted before making a request.)

### Payout Request

<mark style="color:green;">`POST`</mark> <mark style="color:blue;">`baseurl/payout/api/v2/request.php`</mark>

**Headers**

<table><thead><tr><th width="138.27734375">Name</th><th width="100.10546875">Type</th><th width="108.328125">Required</th><th>Value</th></tr></thead><tbody><tr><td>Content-Type</td><td>string</td><td>Required</td><td><code>application/json</code></td></tr><tr><td>X-Api-Key</td><td>string</td><td>Required</td><td><code>Your API Key for authentication</code></td></tr></tbody></table>

**Request Parameters**

<table><thead><tr><th width="174.5703125">Name</th><th width="98.515625">Type</th><th width="112.88671875">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>pid</code></td><td>string</td><td>Required</td><td>Your provided merchant/partner ID</td></tr><tr><td><code>amount</code></td><td>integer</td><td>Required</td><td> Payout amount</td></tr><tr><td><code>order_id</code></td><td>string</td><td>Required</td><td>unique order identifier (minimum 7 characters, alphanumeric)</td></tr><tr><td><code>payment_mode</code></td><td>string</td><td>Required</td><td>Payment method: upi</td></tr><tr><td><code>email</code></td><td>string</td><td>Required</td><td>Recipient's email address</td></tr><tr><td><code>phone</code></td><td>string</td><td>Required</td><td>Recipient's phone number</td></tr><tr><td><code>latitude</code></td><td>string</td><td>Required</td><td>latitude coordinate</td></tr><tr><td><code>longitude</code></td><td>string</td><td>Required</td><td>longitude coordinate</td></tr><tr><td><code>signature</code></td><td>string</td><td>Required</td><td>SHA256 hash for request verification</td></tr><tr><td><code>ip</code></td><td>string</td><td>Required</td><td>Customer's IP address</td></tr><tr><td><code>vpa</code></td><td>string</td><td>Required</td><td>Benificiary UPI Virtual Payment Address (e.g., user@paytm)</td></tr><tr><td><code>account_holder</code></td><td>string</td><td>Required</td><td>Benificiary Account holder name</td></tr></tbody></table>

{% hint style="info" %}
**Important**: <mark style="color:yellow;background-color:yellow;">The signature is mandatory for all requests to ensure security and data integrity.</mark>
{% endhint %}

### Sample Request

{% tabs %}
{% tab title="UPI Payout Request" %}

```json
{
    "pid": "MERCHANT123",
    "amount": 10000,
    "order_id": "ORDER123456789",
    "payment_mode": "upi",
    "email": "user@example.com",
    "phone": "9876543210",
    "latitude": "28.7041",
    "longitude": "77.1025",
    "vpa": "user@paytm",
    "account_holder": "John Doe",
    "signature": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890"
    "ip": "your_ip_address"
}
```

{% endtab %}
{% endtabs %}

### **Sample Response Body**

<mark style="color:green;background-color:green;">**HTTP status**</mark><mark style="color:green;background-color:green;">: 200 OK</mark>

<mark style="color:green;background-color:green;">**Content-Type**</mark><mark style="color:green;background-color:green;">: application/json</mark>

{% tabs %}
{% tab title="Success" %}

```json
{
    "status": "success",
    "ref_code": "a1b2c3d4e5f6789012345678901234567890",
    "message": "Request accepted"
}
```

{% endtab %}

{% tab title="Error" %}

```json
{
    "status": "error",
    "message": "error message"
}
```

{% endtab %}
{% endtabs %}

### Error Response Examples

<mark style="color:red;background-color:red;">**HTTP status**</mark><mark style="color:red;background-color:red;">: 200 OK</mark>

<mark style="color:red;background-color:red;">**Content-Type**</mark><mark style="color:red;background-color:red;">: application/json</mark>

{% hint style="info" %} <mark style="color:$info;">**Note**</mark>

<mark style="color:$warning;">This endpoint always returns an</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">**HTTP 200**</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">status code, even in case of errors.</mark>\ <mark style="color:$warning;">To determine whether the request was successful or not, check the value of the</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">**`status`**</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">field in the JSON response — it will be</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">`"success"`</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">for successful requests and</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">`"error"`</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">for failed ones.</mark>
{% endhint %}

{% tabs %}
{% tab title="Validation Error" %}

```json
{
    "status": "error",
    "message": "pid, amount, order_id, email, phone, latitude, longitude and payment_mode are required fields"
}
```

{% endtab %}

{% tab title="Authentication Error" %}

<pre class="language-json"><code class="lang-json"><strong>{
</strong>    "status": "error",
    "message": "Invalid API key"
<strong>}
</strong></code></pre>

{% endtab %}

{% tab title="Signature Error" %}

```json
{
    "status": "error",
    "message": "Invalid signature"
}
```

{% endtab %}

{% tab title="Insufficient Balance" %}

```json
{
    "status": "error",
    "message": "Low payout wallet balance , Please add fund"
}
```

{% endtab %}

{% tab title="Duplicate Order" %}

```json
{
    "status": "error",
    "message": "Duplicate order_id Found"
}
```

{% endtab %}
{% endtabs %}

### Important Notes

* **Order ID**: Must be unique and atleast 7 characters long, containing only alphanumeric characters, hyphens and underscores
* **Phone Number**: If longer than 10 digits, only the last 10 digits will be used
* **IP Whitelisting:** Your IP address must be whitelisted for production use
* **Rate Limiting:** API calls are rate-limited based on your merchant configuration
* **Signature**: Always include a valid signature for request authentication
* **API Key**: Must be included in the X-Api-Key header for all requests

### Status Codes

<table><thead><tr><th width="164.109375">Status</th><th>Description</th></tr></thead><tbody><tr><td>success</td><td>Payout request accepted and processing</td></tr><tr><td>error</td><td>Request failed due to validation, authentication or system error</td></tr></tbody></table>

## <mark style="color:orange;">CALLBACK</mark>

We trigger your callback URL whenever there is a change in the transaction status.

#### Valid Transaction status are:

1. Approved&#x20;
2. Declined
3. Pending
4. Processing
5. Failed
6. Refunded

#### The most famous transaction changes are (but not limited):&#x20;

1. Pending => Processing
2. Pending => Approved
3. Pending => Declined/Failed
4. Approved => Declined/Failed

The callback landing page must be hosted on your server at a secret path, but it should still be publicly accessible from our whitelisted IPs. (Please ensure you accept data only from our IPs by implementing proper IP whitelisting.)

In the POST body, you will receive the following properties in JSON format:

<table><thead><tr><th>Name</th><th width="186">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>order_id</code></td><td>string</td><td>Your order id shared</td></tr><tr><td><code>requested_amount</code></td><td>int</td><td>requested amount</td></tr><tr><td><code>processed_amount</code></td><td>int</td><td>received amount</td></tr><tr><td><code>bank_ref</code></td><td>string</td><td>bank reference/UTR details if available</td></tr><tr><td><code>sender_pg</code></td><td>string</td><td>sender account name id if available</td></tr><tr><td><code>ref_code</code></td><td>string</td><td>unique code for the transaction</td></tr><tr><td><code>status</code></td><td>string</td><td>status of payment at this time</td></tr><tr><td><code>post_hash</code></td><td>string</td><td>post hash for security verification</td></tr><tr><td><code>payment_type</code></td><td>string</td><td>payment type</td></tr><tr><td><code>request_time</code></td><td>string</td><td>payment request time</td></tr><tr><td><code>action_time</code></td><td>string</td><td>payment made time</td></tr><tr><td><code>upi_vpa</code></td><td>string</td><td>receiver vpa</td></tr><tr><td><code>account_no</code></td><td>string</td><td>receiver account number</td></tr><tr><td><code>account_holder</code></td><td>string</td><td>receiver name</td></tr><tr><td><code>ifsc</code></td><td>string</td><td>receiver IFSC code</td></tr><tr><td><code>bank_name</code></td><td>string</td><td>receiver bank name if available</td></tr><tr><td><code>bank_address</code></td><td>string</td><td>receiver bank address if available</td></tr><tr><td><code>transaction_info</code></td><td>array</td><td>status change log of transaction</td></tr></tbody></table>

**Follow the steps to verify the integrity of received data:**<br>

1. **Capture and Decode the Payload**

Capture the raw JSON data from the POST body and decode it into an array or object.

{% tabs %}
{% tab title="PHP" %}

```php
// Capture and decode the raw POST body
$data = file_get_contents("php://input");
$array = json_decode($data, true);
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
// 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;
```

{% endtab %}

{% tab title="Python" %}

```python
# In a Python framework like Flask, this is
# handled by the request object.
#
# from flask import request
# array = request.get_json()
```

{% endtab %}
{% endtabs %}

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

{% tabs %}
{% tab title="PHP" %}

```php
// 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);
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
// 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');
```

{% endtab %}

{% tab title="Python" %}

```python
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)
```

{% endtab %}
{% endtabs %}

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

{% tabs %}
{% tab title="PHP" %}

```php
//$secret_key is your provided SECRET KEY
$remote_hash = decrypt($ivHashCiphertext, $secret_key);
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
// const secret_key = "YOUR_SECRET_KEY";
const remote_hash = decrypt(ivHashCiphertext, secret_key);
```

{% endtab %}

{% tab title="Python" %}

```python
# secret_key = "YOUR_SECRET_KEY"
remote_hash = decrypt(ivHashCiphertext, secret_key)
```

{% endtab %}
{% endtabs %}

*The `decrypt` function for your language is provided in the reference section below.*

{% tabs %}
{% tab title="PHP" %}

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

{% endtab %}

{% tab title="Node JS" %}

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

{% endtab %}

{% tab title="Python" %}

```python
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 None
```

{% endtab %}
{% endtabs %}

4. **Compute the Local Hash**

Compute the local hash using the MD5 128-bit hashing algorithm. Use the `order_id`, `processed_amount`, and `status` received in the callback array.

**Note**: `processed_amount` may be a `number` or `null`. You must convert it to a string exactly as shown below to match our server's hash.

{% tabs %}
{% tab title="PHP" %}

```php
// Get the values from the same callback $array
$order_id = $array['order_id'];
$processed_amount = (string)$array['processed_amount']; 
$status = $array['status'];
 
$local_hash = md5($order_id . $processed_amount . $status . $secret_key);
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
const crypto = require('crypto');

// Get the values from the same callback array
const order_id = array.order_id;
// Use (value ?? '') to handle both null AND undefined
const processed_amount = String(array.processed_amount ?? ''); 
const status = array.status;
 
const local_hash = crypto
  .createHash('md5')
  .update(order_id + processed_amount + status + secret_key)
  .digest('hex');
```

{% endtab %}

{% tab title="Python" %}

```python
import hashlib

# Get the values from the same callback array (dict)
order_id = array['order_id']
# Use .get() to safely handle a missing key
processed_amount_val = array.get('processed_amount') 
# Convert None to "" and numbers to "100"
processed_amount = "" if processed_amount_val is None else str(processed_amount_val)

status = array['status']
 
local_hash = hashlib.md5((order_id + processed_amount + status + secret_key).encode('utf-8')).hexdigest()
```

{% endtab %}
{% endtabs %}

5. **Verify Hash**

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

{% tabs %}
{% tab title="PHP" %}

```php
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";  
}
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
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"
}
```

{% endtab %}

{% tab title="Python" %}

```python
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"
```

{% endtab %}
{% endtabs %}

6. **Acknowledge the Payment Gateway**

To confirm you have received the callback and to prevent our gateway from sending retries, you must do two things:

* &#x20;Respond with an HTTP 200 OK status code.
* Respond with a JSON body containing the key `"acknowledge"` set to the string `"yes"`.

{% tabs %}
{% tab title="PHP" %}

```php
// --- 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;
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
// 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
});
```

{% endtab %}

{% tab title="Python" %}

```python
# 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), 200
```

{% endtab %}
{% endtabs %}

## <mark style="color:orange;">STATUS POLLING :</mark>

The payout Status Polling API allows you to retrieve real-time status information for payout transactions. This endpoint provides comprehensive transaction details including payment status, amounts and verification hashes.

**Note** : <mark style="color:yellow;">All requests must include proper authentication headers and IP whitelisting may be required for production environments.</mark>

<mark style="color:green;">`POST`</mark> <mark style="color:blue;">`{baseurl}/payout/api/v2/status_polling.php`</mark>

### Authentication

This API required the following authentication mechanism:

#### Required Headers

<table><thead><tr><th width="143.51171875">Header</th><th width="103.8984375">Type</th><th width="112.71875">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>Content-Type</code></td><td>string</td><td>Yes</td><td>Must be <code>application/json</code></td></tr><tr><td><code>X-Api-Key</code></td><td>string</td><td>Yes</td><td>Your provided API key for authentication</td></tr></tbody></table>

#### IP Whitelisting

Your server IP address must be whitelisted in production environments. Contact your administration to ensure your IP is properly configured.

#### **Request Parameters**

<table><thead><tr><th>Parameter</th><th width="143">Type</th><th>Required</th><th width="242">Description</th></tr></thead><tbody><tr><td><code>pid</code></td><td>string</td><td>Yes</td><td>Your unique merchant/partner ID</td></tr><tr><td><code>ref_code</code></td><td>string</td><td>Yes</td><td>unique ref_code which is generated in payout request</td></tr><tr><td><code>post_hash</code></td><td>string</td><td>Yes</td><td>The Base64-encoded encrypted hash. (See steps below).</td></tr></tbody></table>

**Steps to generate post\_hash :**&#x20;

1. **Generate the Request** `post_hash`&#x20;

The following code shows the complete flow of creating the MD5 hash, encrypting it, and Base64-encoding it to get the final `post_hash` string.

{% tabs %}
{% tab title="PHP" %}

```php
$ref_code = "YOUR_REF_CODE";
$pid = "YOUR_PID";
$secret_key = "YOUR_SECRET_KEY";

// 1.1 Create Plaintext Hash
$local_hash = md5($ref_code . $pid . $secret_key);

// 1.2 Encrypt the Hash
// (Assumes encrypt() function is defined from reference)
$encrypted_hash = encrypt($local_hash, $secret_key);

// 1.3 Base64 Encode
$post_hash = base64_encode($encrypted_hash);

// $post_hash is now ready to be sent
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
const crypto = require('crypto');
// Assumes 'encrypt' function is imported from reference
// const { encrypt } = require('./crypto-helpers');

const ref_code = "YOUR_REF_CODE";
const pid = "YOUR_PID";
const secret_key = "YOUR_SECRET_KEY";

// 1.1 Create Plaintext Hash
const local_hash = crypto
  .createHash('md5')
  .update(ref_code + pid + secret_key)
  .digest('hex');

// 1.2 Encrypt the Hash
// 'encrypt' function returns a raw Buffer
const encrypted_hash_buffer = encrypt(local_hash, secret_key);

// 1.3 Base64 Encode
const post_hash = encrypted_hash_buffer.toString('base64');

// post_hash is now ready to be sent
```

{% endtab %}

{% tab title="Python" %}

```python
import hashlib
import base64
# Assumes 'encrypt' function is defined from reference
# from crypto_helpers import encrypt

ref_code = "YOUR_REF_CODE"
pid = "YOUR_PID"
secret_key = "YOUR_SECRET_KEY"

# 1.1 Create Plaintext Hash
local_hash = hashlib.md5((ref_code + pid + secret_key).encode('utf-8')).hexdigest()

# 1.2 Encrypt the Hash
# 'encrypt' function returns raw bytes
encrypted_hash_bytes = encrypt(local_hash, secret_key)

# 1.3 Base64 Encode
post_hash = base64.b64encode(encrypted_hash_bytes).decode('utf-8')

# post_hash is now ready to be sent
```

{% endtab %}
{% endtabs %}

*Refer the `encrpt` function below of your language:*

{% tabs %}
{% tab title="PHP" %}

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

{% endtab %}

{% tab title="Python" %}

```python
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 + ciphertext
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
const 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]);
}
```

{% endtab %}
{% endtabs %}

2. &#x20;**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.

{% tabs %}
{% tab title="PHP" %}

```php
<?php
// --- Step 1: Data (from above) ---
$pid = "YOUR_PID";
$ref_code = "YOUR_REF_CODE";
$secret_key = "YOUR_SECRET_KEY";
$api_key = "YOUR_API_KEY_HERE";

$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>/payout/api/v2/status_polling.php";

$data = [
    'pid' => $pid,
    'ref_code' => $ref_code,
    'post_hash' => $post_hash
];

$headers = [
    'Content-Type: application/json',
    'X-Api-Key: ' . $api_key
];

$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, $headers);

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

// $server_output now contains the JSON response
?>
```

{% endtab %}

{% tab title="Python" %}

```python
# Requires the 'requests' library: pip install requests
import requests
import json
# Assumes 'post_hash' was generated in Step 1

api_url = "https://<domain>/payout/api/v2/status_polling.php"

data = {
    'pid': "YOUR_PID",
    'ref_code': "YOUR_REF_CODE",
    'post_hash': post_hash  # The string from Step 1
}

headers = {
    'Content-Type': 'application/json',
    'X-Api-Key': 'YOUR_API_KEY_HERE'
}

try:
    response = requests.post(api_url, data=json.dumps(data), headers=headers)
    response.raise_for_status() # Raises an error for bad responses (4xx, 5xx)
    
    server_output = response.json()
    # server_output now contains the parsed JSON response
    print(server_output)
    
except requests.exceptions.HTTPError as err:
    print(f"HTTP error: {err.response.text}")
except Exception as err:
    print(f"An error occurred: {err}")
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
// Requires the 'axios' library: npm install axios
const axios = require('axios');
// Assumes 'post_hash' was generated in Step 1

const api_url = "https://<domain>/payout/api/v2/status_polling.php";

const data = {
    pid: "YOUR_PID",
    ref_code: "YOUR_REF_CODE",
    post_hash: post_hash // The string from Step 1
};

const headers = {
    'Content-Type': 'application/json',
    'X-Api-Key': 'YOUR_API_KEY_HERE'
};

async function pollStatus() {
    try {
        const response = await axios.post(api_url, data, { headers: headers });
        const server_output = response.data;
        // server_output now contains the JSON response
        console.log(server_output);
    } catch (error) {
        console.error("Error polling status:", 
            error.response ? error.response.data : error.message
        );
    }
}

pollStatus();
```

{% endtab %}
{% endtabs %}

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

#### <sub>Success Response Parameters</sub>

A successful response will contain the following parameters as JSON body:

| `order_id`         | String | Your order id shared                                  |
| ------------------ | ------ | ----------------------------------------------------- |
| `requested_amount` | Number | Requested amount (as a float)                         |
| `processed_amount` | Number | Processed amount (as a float, e.g., `100.0` or `0.0`) |
| `bank_reference`   | String | Bank reference/UTR details if available               |
| `ref_code`         | String | Unique code for the transaction                       |
| `status`           | String | Status of payment at this time                        |
| `time`             | Number | Raw Unix timestamp of the last action                 |
| `payment_type`     | String | Payment type (e.g., `UPI`, `IMPS`)                    |
| `request_time`     | String | Payment request time (ISO 8601 format)                |
| `action_time`      | String | Payment made time (ISO 8601 format)                   |
| `upi_vpa`          | String | Receiver VPA if available                             |
| `account_no`       | String | Receiver account number                               |
| `account_holder`   | String | Receiver name                                         |
| `ifsc`             | String | Receiver IFSC code                                    |
| `bank_name`        | String | Receiver bank name if available                       |
| `bank_address`     | String | Receiver bank address if available                    |
| `transaction_info` | Array  | Status change log of the transaction                  |
| `post_hash`        | String | Post hash for security verification                   |

#### <sub>Error Response</sub>

If your request fails, the server will respond with an appropriate HTTP status code and a JSON error body.

```json
{
    "error": "error message"
}
```

**Common Errors and HTTP status**

| HTTP Status | Error Message                                             | Description                                                  |
| ----------- | --------------------------------------------------------- | ------------------------------------------------------------ |
| `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 base64 encoding in post_hash`                    | The `post_hash` string was not valid Base64.                 |
| `400`       | `Invalid hash`                                            | The signature verification failed. The hashes do not match.  |
| `400`       | `Reference code not found`                                | The requested `ref_code` does not exist for this PID.        |
| `4S1`       | `Invalid PID`                                             | The `pid` sent does not exist in our system.                 |
| `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 request white list your IP: ...` | Your server's IP is not in the payout whitelist.             |
| `429`       | `[Dynamic rate limit message]`                            | You have exceeded the allowed request rate.                  |
| `500`       | `Database connection failed`                              | A server-side error occurred. Please contact support.        |

4. **Verify the Response** `post_hash`

Before trusting any data from the response, you must verify its `post_hash`. 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`, `processed_amount`, and `status`. Concatenate them with your `secret_key` and create an MD5 hash.

Note: The `processed_amount` is a float in the response. The code below correctly handles converting it to a string to match our server's hash.

{% tabs %}
{% tab title="PHP" %}

```php
// $response_data is the decoded JSON response
$order_id = $response_data['order_id'];
// PHP converts the float (e.g., 100.0) to "100"
$processed_amount = $response_data['processed_amount'];
$status = $response_data['status'];
 
$local_hash = md5($order_id . $processed_amount . $status . $secret_key);
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
const crypto = require('crypto');

// responseData is the parsed JSON response
const order_id = responseData.order_id;
// Node.js converts the float (e.g., 100.0) to "100"
const processed_amount = responseData.processed_amount;
const status = responseData.status;
 
const local_hash = crypto
  .createHash('md5')
  .update(order_id.toString() + processed_amount.toString() + status.toString() + secret_key)
  .digest('hex');
```

{% endtab %}

{% tab title="Python" %}

```python
import hashlib
 
# response_data is the parsed JSON response (a dict)
order_id = response_data['order_id']
 
# CRITICAL: Use "%g" to format the float (e.g., 100.0)
# to "100", matching our server-side logic.
# Using str(100.0) would fail (it becomes "100.0").
processed_amount = "%g" % response_data['processed_amount']
 
status = response_data['status']
 
local_hash = hashlib.md5((order_id + processed_amount + status + secret_key).encode('utf-8')).hexdigest()
```

{% endtab %}
{% endtabs %}

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

{% tabs %}
{% tab title="PHP" %}

```php
// 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);
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
// Get the Base64 string from the response
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);
```

{% endtab %}

{% tab title="Python" %}

```python
import base64

# Get the Base64 string from the response
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)
```

{% endtab %}
{% endtabs %}

*The `decrypt` function for your language is provided in the reference section below.*

{% tabs %}
{% tab title="PHP" %}

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

{% endtab %}

{% tab title="Node JS" %}

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

{% endtab %}

{% tab title="Python" %}

```python
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 None
```

{% endtab %}
{% endtabs %}

**4.3 Compare the Hashes**

Securely compare the `local_hash` you just computed with the `remote_hash` you decrypted.

{% tabs %}
{% tab title="PHP" %}

```php
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.
}
```

{% endtab %}

{% tab title="Node JS" %}

```javascript
let isVerified = false;
if (remote_hash) {
    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) {
        isVerified = false;
    }
}

if (isVerified) {
    // --- SUCCESS: Data is verified ---
    // console.log("Status:", responseData.status);
} else {
    // --- FAILURE: Hash mismatch! ---
}
```

{% endtab %}

{% tab title="Python" %}

```python
import hmac

isVerified = False
if remote_hash:
    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! ---
    pass
```

{% endtab %}
{% endtabs %}

## <mark style="color:orange;">TRANSACTION STATUS :</mark>&#x20;

1. **Approved:** Payment has been successfully approved by our system.
2. **Failed:** Payment failed on the bank's side.
3. **Processing:** The bank is currently processing the payment.
4. **Declined:** Payment has been declined by our system.
5. **Pending:** The user session is active, and the payment is awaiting completion.
6. **Refunded** : The amount is refunded to customer

## <mark style="color:orange;">ERROR :</mark>&#x20;

1. **No valid channel found** : This error raise if no active channel/provider mapped against your MID
2. **Bank error , please contact admin** : Unknown bank side error
3. **Hash value is not defined** : This error raises for status check if the provided hash is not matched with genereted one
4. **Ref code does not exist** : This error raise in the status check , if ref\_code is not provided for status check

## <mark style="color:orange;">COMPLAINT</mark>

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](https://doc.payatom.in/api-integration/v1/complaint)

## <mark style="color:orange;">RECONCILIATION</mark>

This API endpoint allows authorized users to retrieve approved payout 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](https://doc.payatom.in/api-integration/v1/payout-reconciliation)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://doc.payatom.in/api-integration/v2/india/payout-seamless-upi-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
