YayaPay API Documentation
Complete integration reference for Pay-In (deposit) and Pay-Out (withdrawal) via UPI, Bank Transfer, and USDT TRC20.
Introduction
Welcome to the YayaPay Developer API. This gateway provides INR payment collection (Pay-In) and merchant payout (Pay-Out) via UPI, Bank Transfer, and USDT TRC20.
Pay-In requests use form-encoded (application/x-www-form-urlencoded) POST. Pay-Out requests use JSON (application/json) POST. All responses are JSON.
USDT TRC20 Payouts are now supported. Use withdraw_type: "usdt" with a valid TRC20 wallet address. Fixed rate: 1 USDT = ₹118 INR. Minimum payout: ₹118.
Pay-Out API is only available for Unlimited Merchant accounts. Please upgrade your plan in the merchant dashboard to access payout functionality.
Authentication
All API requests are authenticated using your user_token, a unique token assigned to your merchant account. Retrieve it from your merchant dashboard.
For Pay-In, pass user_token as a form field. For Pay-Out, include it in the JSON request body.
Never expose your user_token in client-side code, HTML, or public repositories. Always make API calls from your server backend only.
Pay-In (Deposit)
Create a payment order for your customer. On success, redirect the customer to the returned payment_url to complete payment.
Requests must use form-encoded body: Content-Type: application/x-www-form-urlencoded
| Field | Type | Required | Description |
|---|---|---|---|
user_token | string | Required | Unique token assigned to the merchant. |
customer_mobile | string | Required | Customer's mobile number. |
amount | string | Required | Transaction amount in INR. |
order_id | string | Required | Unique order identifier from your system. Must not be reused. |
redirect_url | string | Required | URL where the user is redirected after payment. |
remark1 | string | Optional | First remark or reference note. |
remark2 | string | Optional | Second remark or reference note. |
{
"status": true, // true = success, false = failed
"msg": "Order Created Successfully",
"data": {
"orderId": "1234561705047510",
"payment_url": "https://yayapay.shop/pay/..." // redirect customer here
}
}
{
"status": false,
"msg": "Missing required fields" // read msg to find out why
}
Pay-Out (Withdrawal)
Programmatically initiate payouts to your users via UPI, Bank Transfer, or USDT TRC20. Payout requests are queued as PENDING and processed after admin approval.
Requests must use JSON body: Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
user_token | string | Required | Your Merchant User Token from dashboard. |
payout_order_id | string | Required | Unique payout reference ID from your system. Must be unique per user. |
amount | float | Required | Payout amount in INR. Minimum ₹100 (UPI/Bank) or ₹118 (USDT). Use plain float, e.g. 500 or 500.50. |
withdraw_type | string | Required | Transfer mode. Accepted: upi, bank, or usdt. |
redirect_url | string | Optional | Callback URL (POST) notified after admin approval or rejection. |
remark1 | string | Optional | Custom remark (e.g. Vendor Payment). |
remark2 | string | Optional | Additional remark (e.g. March Invoice). |
Required when withdraw_type is upi.
| Field | Type | Required | Description |
|---|---|---|---|
upi_id | string | Required | Recipient UPI ID. Format: username@bank or mobile@upie.g. user@paytm, 9876543210@ybl |
Required when withdraw_type is bank.
| Field | Type | Required | Description |
|---|---|---|---|
account_holder | string | Required | Account holder name as per bank records. |
account_number | string | Required | Bank account number (9–18 digits). |
ifsc_code | string | Required | IFSC code. Format: HDFC0001234 |
bank_name | string | Required | Name of the bank (e.g. HDFC Bank, SBI). |
branch_name | string | Optional | Branch name (e.g. Kolkata Main Branch). |
Required when withdraw_type is usdt. The payout amount is entered in INR and converted to USDT at the fixed platform rate.
| Field | Type | Required | Description |
|---|---|---|---|
trc20_address |
string | Required |
Recipient USDT TRC20 wallet address. Must start with T and be exactly 34 characters (Base58 encoded). e.g. TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE |
How USDT conversion works: You specify the INR amount (e.g. ₹1,180). The fee (3% + ₹6) is deducted from your INR balance. The requested amount is then divided by ₹118 to calculate USDT sent to the wallet.
Example: Request ₹1,180 → Fee = ₹35.40 + ₹6 = ₹41.40 → Total deducted = ₹1,221.40 → Recipient gets 10.000000 USDT
The following fees apply to all three payout types (UPI, Bank, USDT). Fee is always calculated and deducted in INR:
UPI / Bank example: ₹500 payout → Fee ₹15 + ₹6 = ₹21 → Total deducted ₹521
USDT example: ₹1,180 payout → Fee ₹35.40 + ₹6 = ₹41.40 → Total deducted ₹1,221.40 → Recipient gets 10.000000 USDT
{
"user_token": "your_user_token_here",
"payout_order_id": "PAYOUT_20260302_120000",
"amount": 500,
"withdraw_type": "upi",
"upi_id": "merchant@paytm",
"redirect_url": "https://yoursite.com/payout-callback",
"remark1": "Vendor Payment",
"remark2": "March Invoice"
}
{
"user_token": "your_user_token_here",
"payout_order_id": "PAYOUT_20260302_120001",
"amount": 1000,
"withdraw_type": "bank",
"account_holder": "Rahul Sharma",
"account_number": "123456789012",
"ifsc_code": "HDFC0001234",
"bank_name": "HDFC Bank",
"branch_name": "Kolkata Main Branch",
"redirect_url": "https://yoursite.com/payout-callback"
}
{
"user_token": "your_user_token_here",
"payout_order_id": "PAYOUT_USDT_20260302_120002",
"amount": 1180, // INR amount — must be ≥ ₹118
"withdraw_type": "usdt",
"trc20_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE", // 34-char TRC20 address
"redirect_url": "https://yoursite.com/payout-callback",
"remark1": "Crypto Withdrawal"
}
{
"status": true,
"message": "Payout request created successfully",
"result": {
"payout_id": 12,
"payout_order_id": "PAYOUT_20260302_120000",
"withdraw_type": "upi",
"amount": 500,
"fee": 21,
"total_deducted": 521,
"status": "PENDING",
"available_balance_after": 4479,
"request_date": "2026-03-02 12:00:00"
}
}
USDT payouts include additional fields: trc20_address, usdt_amount, usdt_rate, and usdt_rate_note.
{
"status": true,
"message": "Payout request created successfully",
"result": {
"payout_id": 13,
"payout_order_id": "PAYOUT_USDT_20260302_120002",
"withdraw_type": "usdt",
"amount": 1180, // INR amount requested
"fee": 41.4, // 3% (₹35.40) + ₹6
"total_deducted": 1221.4, // deducted from INR balance
"status": "PENDING",
"available_balance_after": 3778.6,
"trc20_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"usdt_amount": 10.000000, // USDT sent to wallet (1180 ÷ 118)
"usdt_rate": 118, // fixed rate used
"usdt_rate_note": "1 USDT = ₹118 (fixed rate)",
"request_date": "2026-03-02 12:00:00"
}
}
{
"status": false,
"message": "Insufficient balance",
"details": {
"available_balance": 300.00,
"requested_amount": 500.00,
"fee": 21.00,
"total_deducted": 521.00,
// For USDT requests, these extra fields are also returned:
"usdt_equivalent": 4.237288, // USDT that would have been sent
"usdt_rate": 118
}
}
Payout Lifecycle
After submitting a payout (UPI, Bank, or USDT), it goes through the following states. A callback POST is sent to your redirect_url upon approval or rejection.
| Status | Meaning |
|---|---|
| PENDING | Payout submitted, awaiting admin review. Funds reserved from your balance. |
| APPROVED | Payout approved and processed by admin. For USDT, the TRC20 transfer is dispatched to the wallet address. |
| REJECTED | Payout rejected by admin. Balance may be restored depending on policy. |
After approval or rejection, a callback POST is sent to your redirect_url. See the Payout Callback Docs for full payload details.
PHP Examples
$params = http_build_query([ 'user_token' => 'YOUR_USER_TOKEN', 'customer_mobile' => '9876543210', 'amount' => '500', 'order_id' => 'ORD-' . time(), 'redirect_url' => 'https://yoursite.com/payment-return', 'remark1' => 'Wallet Deposit', ]); $ch = curl_init('https://yayapay.shop/api/create-order'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $params, CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if ($response['status'] === true) { header('Location: ' . $response['data']['payment_url']); exit; } else { echo 'Pay-In Error: ' . $response['msg']; }
$payload = [ 'user_token' => 'YOUR_USER_TOKEN', 'payout_order_id' => 'PAYOUT-' . time(), 'amount' => 500, 'withdraw_type' => 'upi', 'upi_id' => 'user@paytm', 'redirect_url' => 'https://yoursite.com/payout-callback', 'remark1' => 'Withdrawal', ]; $ch = curl_init('https://yayapay.shop/api/create_payout'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if ($response['status'] === true) { $result = $response['result']; echo 'Payout queued. ID: ' . $result['payout_id']; echo ' | Balance after: ₹' . $result['available_balance_after']; } else { echo 'Payout Error: ' . $response['message']; }
$payload = [ 'user_token' => 'YOUR_USER_TOKEN', 'payout_order_id' => 'PAYOUT-BANK-' . time(), 'amount' => 1000, 'withdraw_type' => 'bank', 'account_holder' => 'Rahul Sharma', 'account_number' => '123456789012', 'ifsc_code' => 'HDFC0001234', 'bank_name' => 'HDFC Bank', 'branch_name' => 'Mumbai Main Branch', 'redirect_url' => 'https://yoursite.com/payout-callback', ]; $ch = curl_init('https://yayapay.shop/api/create_payout'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if ($response['status'] === true) { echo 'Payout queued. ID: ' . $response['result']['payout_id']; } else { echo 'Error: ' . $response['message']; }
// Fixed rate: 1 USDT = ₹118 $inr_amount = 1180; // INR to withdraw (must be ≥ ₹118) $trc20_address = 'TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE'; // Validate TRC20 address before sending if (!preg_match('/^T[1-9A-HJ-NP-Za-km-z]{33}$/', $trc20_address)) { die('Invalid TRC20 address'); } $payload = [ 'user_token' => 'YOUR_USER_TOKEN', 'payout_order_id' => 'PAYOUT-USDT-' . time(), 'amount' => $inr_amount, 'withdraw_type' => 'usdt', 'trc20_address' => $trc20_address, 'redirect_url' => 'https://yoursite.com/payout-callback', 'remark1' => 'Crypto Withdrawal', ]; $ch = curl_init('https://yayapay.shop/api/create_payout'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if ($response['status'] === true) { $r = $response['result']; echo 'Payout queued. ID: ' . $r['payout_id'] . "\n"; echo 'USDT to be sent: ' . $r['usdt_amount'] . ' USDT' . "\n"; echo 'Wallet: ' . $r['trc20_address'] . "\n"; echo 'INR deducted: ₹' . $r['total_deducted'] . "\n"; echo 'Balance remaining: ₹' . $r['available_balance_after']; } else { echo 'USDT Payout Error: ' . $response['message']; }
Pay-In Webhook & Callback
When a customer completes a deposit, the gateway fires a server-to-server GET/POST to your webhook.php. The handler credits the user's balance, marks the order processed, then sends a signed callback POST to the callback_url you stored on the order — so your application can react in real time (update UI, send push notification, award bonus, etc.).
Callback fires after the database COMMIT — a callback delivery failure will never roll back the user's balance credit.
Add a callback_url column to your thevani (orders) table. Run this once:
ALTER TABLE recharge ADD COLUMN callback_url VARCHAR(500) DEFAULT NULL AFTER status; -- Optional: idempotency table to prevent double-processing on gateway retries CREATE TABLE processed_callbacks ( id INT AUTO_INCREMENT PRIMARY KEY, order_id VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
When you create a new deposit order in thevani, store your application's callback URL so the webhook can retrieve it later:
$callbackUrl = 'https://yoursite.com/payment-callback'; $stmt = $conn->prepare( "INSERT INTO recharge (orderid, balance, userid, status, callback_url) VALUES (?, ?, ?, '0', ?)" ); $stmt->bind_param('sdss', $orderId, $amount, $userId, $callbackUrl); $stmt->execute();
The gateway delivers these parameters to your webhook.php via GET or POST:
| Parameter | Type | Required | Description |
|---|---|---|---|
order_id | string | Required | Unique order identifier from the gateway. |
status | string | Required | SUCCESS for a completed payment, or a failure string. |
remark1 | string | Optional | Remark forwarded from the gateway — passed along in the callback. |
| Response | Scenario |
|---|---|
ok2 | Order found, status=SUCCESS — balance credited successfully. |
ok1 | Order not found, already processed, or non-SUCCESS status. |
{"status":false,"message":"Missing parameters"} | order_id or status was empty. |
{"status":false,"message":"Transaction failed"} | Database error — transaction rolled back. |
// ── Config ────────────────────────────────────────────────── define('DB_HOST', 'localhost'); define('DB_USERNAME', 'username'); define('DB_PASSWORD', 'password'); define('DB_NAME', 'dbname'); define('CALLBACK_SECRET', 'YOUR_SECRET_KEY_HERE'); // change this // ── Connection ─────────────────────────────────────────────── $conn = new mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME); if ($conn->connect_error) { die("Connection failed: " . $conn->connect_error); } $conn->set_charset("utf8"); // ── Callback dispatcher ────────────────────────────────────── // Sends a signed POST to callback_url stored on the order row. // Failure is logged — never rolls back the DB transaction. function sendCallback( string $url, string $orderId, string $status, float $amount, string $userId, string $remark1 ): bool { if (empty($url)) { writeLog("Callback skipped: no callback_url for order $orderId"); return false; } $ts = time(); $sig = hash_hmac('sha256', $orderId . $status . $amount . $ts, CALLBACK_SECRET); $payload = json_encode([ 'order_id' => $orderId, 'status' => $status, // "SUCCESS" or "FAILED" 'amount' => $amount, 'user_id' => $userId, 'remark1' => $remark1, 'timestamp' => $ts, 'signature' => $sig, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-Webhook-Signature: ' . $sig, ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_FOLLOWLOCATION => false, ]); $res = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($err) { writeLog("Callback cURL error [$orderId]: $err"); return false; } writeLog("Callback [$orderId] → $url | HTTP $code | " . substr($res, 0, 200)); return ($code >= 200 && $code < 300); } // ── Log raw incoming data ──────────────────────────────────── writeLog("Received: " . print_r($_GET, true) . print_r($_POST, true)); // ── Collect parameters (GET or POST) ──────────────────────── $order_id = trim($_GET['order_id'] ?? $_POST['order_id'] ?? ''); $status = trim($_GET['status'] ?? $_POST['status'] ?? ''); $remark1 = trim($_GET['remark1'] ?? $_POST['remark1'] ?? ''); if (empty($order_id) || empty($status)) { writeLog("Error: Missing parameters"); echo json_encode(['status' => false, 'message' => 'Missing parameters']); exit; } if (!$checkamt) { writeLog("DB Error: " . $conn->error); echo json_encode(['status' => false, 'message' => 'Database error']); exit; } else { writeLog("Notice: Order $order_id not found / already done / status=$status"); echo "ok1"; } $conn->close(); exit;
After a successful commit, your callback_url receives this signed JSON via POST:
{
"order_id": "ORD-1234567890",
"status": "SUCCESS", // "SUCCESS" or "FAILED"
"amount": 500.00,
"user_id": "USR-001",
"remark1": "Wallet Deposit",
"timestamp": 1743479400, // Unix epoch
"signature": "a3f9c1d2e4b7..." // HMAC-SHA256 — verify this
}
| Field | Type | Description |
|---|---|---|
order_id | string | Original order ID from thevani.dharavahi |
status | string | SUCCESS — balance credited. FAILED — transaction rolled back. |
amount | float | Amount credited to user (INR). |
user_id | string | Internal user ID (balakedara). |
remark1 | string | Remark forwarded from the gateway. |
timestamp | int | Unix epoch when callback was fired. Use for replay-attack protection. |
signature | string | HMAC-SHA256 of order_id + status + amount + timestamp. Always verify. |
Create this file at your callback_url path. It verifies the signature, prevents replay attacks, and processes the payment notification:
$body = file_get_contents('php://input'); $data = json_decode($body, true); $secret = 'YOUR_SECRET_KEY_HERE'; // same as CALLBACK_SECRET in webhook.php // 1. Replay-attack protection: reject if older than 5 minutes if (abs(time() - ($data['timestamp'] ?? 0)) > 300) { http_response_code(400); exit('Expired timestamp'); } // 2. Verify HMAC signature $raw = $data['order_id'] . $data['status'] . $data['amount'] . $data['timestamp']; $expected = hash_hmac('sha256', $raw, $secret); if (!hash_equals($expected, $data['signature'] ?? '')) { http_response_code(401); exit('Invalid signature'); } // 3. Idempotency — prevent double-processing on gateway retries $stmt = $conn->prepare("SELECT id FROM processed_callbacks WHERE order_id = ?"); $stmt->bind_param('s', $data['order_id']); $stmt->execute(); if ($stmt->get_result()->num_rows > 0) { http_response_code(200); exit('Already processed'); } // 4. Handle the payment result if ($data['status'] === 'SUCCESS') { // ✅ send push notification, update UI, award bonus, etc. } // 5. Mark as processed (idempotency record) $stmt = $conn->prepare("INSERT INTO processed_callbacks (order_id) VALUES (?)"); $stmt->bind_param('s', $data['order_id']); $stmt->execute(); http_response_code(200); echo 'ok';
Always use hash_equals() to compare signatures — not == or ===. This prevents timing-based signature forgery attacks.
Your callback endpoint must respond with HTTP 200 within 10 seconds. If it does not, the delivery is marked failed in logs. The user balance is not affected — it was already committed.
Payout Callback
The payout callback system gives your server two ways to track payout outcomes: automatic push via your redirect_url (called once the payout is approved or rejected), and on-demand pull via a GET status endpoint you can call anytime from your backend.
Both flows share the same endpoint file: /api/payout_callback.php. Use the GET method to poll status, and implement a POST receiver on your server to handle the automatic push.
Supply a redirect_url when creating a payout and YayaPay will automatically POST the final payload to it once approved or rejected. Use the GET Status endpoint below to proactively poll if you haven't received a callback yet.
When admin approves or rejects your payout, a JSON POST is fired to your redirect_url. The payload contains all fields below. Method-specific fields (UPI / Bank / USDT) are appended automatically based on withdraw_type.
| Field | Type | Description |
|---|---|---|
event | string | payout.approved or payout.rejected |
status | string | APPROVED or REJECTED |
payout_id | integer | Internal payout record ID. |
payout_order_id | string | Your original payout reference ID passed at creation. |
user_token | string | Merchant user token used to create the payout. |
withdraw_type | string | upi, bank, or usdt |
amount | float | Requested payout amount in INR. |
fee | float | Fee charged (3% + ₹6) in INR. |
total_deducted | float | Total INR deducted from balance (amount + fee). |
available_balance_after | float | Remaining merchant balance after this payout. |
request_date | string | Date & time payout was requested (IST). |
response_date | string | Date & time admin approved or rejected (IST). |
admin_remarks | string|null | Admin-entered notes. Check this on REJECTED events for the rejection reason. |
remark1 | string|null | Your custom remark passed at payout creation. |
remark2 | string|null | Your secondary remark passed at payout creation. |
message | string | Human-readable status string including amount and reason. |
upi_id | string | UPI only. The UPI ID the funds were sent to. |
account_holder, account_number, ifsc_code, bank_name, branch_name | string | Bank only. Destination bank account details. |
trc20_address | string | USDT only. Recipient TRC20 wallet address. |
usdt_amount | float | USDT only. USDT sent (INR amount ÷ 118), 6 decimal places. |
usdt_rate | float | USDT only. Fixed rate used: 118.00 |
usdt_rate_note | string | USDT only. Human label: 1 USDT = ₹118 |
{
"event": "payout.approved",
"status": "APPROVED",
"payout_id": 5,
"payout_order_id": "PAYOUT_20260302_120000",
"user_token": "your_user_token_here",
"withdraw_type": "upi",
"amount": 500,
"fee": 21,
"total_deducted": 521,
"available_balance_after": 4479,
"request_date": "2026-03-02 12:00:00",
"response_date": "2026-03-02 12:05:00",
"admin_remarks": "Processed",
"remark1": "Vendor Payment",
"remark2": "March Invoice",
"upi_id": "merchant@paytm",
"message": "Your payout of ₹500.00 has been approved and processed successfully."
}
{
"event": "payout.rejected",
"status": "REJECTED",
"payout_id": 6,
"payout_order_id":"PAYOUT_20260302_130000",
"withdraw_type": "bank",
"amount": 1000,
"fee": 36,
"total_deducted": 1036,
"admin_remarks": "Invalid account number", // always check this on REJECTED
"message": "Your payout of ₹1000.00 has been rejected. Reason: Invalid account number"
}
{
"event": "payout.approved",
"status": "APPROVED",
"payout_id": 13,
"payout_order_id": "PAYOUT_USDT_20260302_120002",
"withdraw_type": "usdt",
"amount": 1180,
"fee": 41.4,
"total_deducted": 1221.4,
"available_balance_after": 8778.6,
"request_date": "2026-03-02 12:00:00",
"response_date": "2026-03-02 12:08:00",
"remark1": "Crypto Withdrawal",
"trc20_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"usdt_amount": 10.000000,
"usdt_rate": 118,
"usdt_rate_note": "1 USDT = ₹118",
"message": "Your payout of ₹1180.00 (≈ 10.000000 USDT TRC20) has been approved and processed successfully."
}
Query the live status of any payout from your backend at any time. Identify by payout_order_id (your reference ID) or the internal payout_id. Always pass your user_token.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_token | string | Required | Your merchant user token. Scopes the lookup to your account only. |
payout_order_id | string | Required* | Your original payout reference ID. Use this or payout_id. |
payout_id | integer | Required* | Internal numeric payout ID returned at creation. Use this or payout_order_id. |
* Provide either payout_order_id or payout_id — at least one is required.
curl -X GET "https://yayapay.shop/api/payout_callback.php?user_token=your_user_token_here&payout_order_id=PAYOUT_20260302_120000"
curl -X GET "https://yayapay.shop/api/payout_callback.php?user_token=your_user_token_here&payout_id=5"
{
"status": true,
"payout_id": 5,
"payout_order_id": "PAYOUT_20260302_120000",
"payout_status": "APPROVED", // PENDING | APPROVED | REJECTED
"withdraw_type": "upi",
"amount": 500,
"fee": 21,
"total_deducted": 521,
"request_date": "2026-03-02 12:00:00",
"response_date": "2026-03-02 12:05:00",
"admin_remarks": "Processed",
"remark1": "Vendor Payment",
"remark2": "March Invoice",
"upi_id": "merchant@paytm",
"message": "Your payout of ₹500.00 has been approved and processed successfully."
}
Host this script at your redirect_url. It receives the incoming JSON payload, reads the status, and lets you take action — notify the user, update your DB, restore balance on rejection, etc.
<?php // ── Read raw JSON body ─────────────────────────────────────── $raw = file_get_contents('php://input'); $data = json_decode($raw, true); if (empty($data)) { http_response_code(400); exit('Invalid payload'); } // ── Extract key fields ─────────────────────────────────────── $event = $data['event'] ?? ''; // payout.approved | payout.rejected $payout_status = $data['status'] ?? ''; // APPROVED | REJECTED $payout_id = $data['payout_id'] ?? 0; $payout_order_id = $data['payout_order_id'] ?? ''; $withdraw_type = $data['withdraw_type'] ?? ''; $amount = floatval($data['amount'] ?? 0); $admin_remarks = $data['admin_remarks'] ?? ''; $user_token = $data['user_token'] ?? ''; // ── Basic validation ───────────────────────────────────────── if (empty($payout_order_id) || empty($payout_status)) { http_response_code(400); exit('Missing required fields'); } // ── Handle USDT-specific fields ────────────────────────────── if ($withdraw_type === 'usdt') { $trc20_address = $data['trc20_address'] ?? ''; $usdt_amount = floatval($data['usdt_amount'] ?? 0); $usdt_rate = floatval($data['usdt_rate'] ?? 118); } // ── Act on status ──────────────────────────────────────────── switch ($payout_status) { case 'APPROVED': // Mark payout as complete in your DB // Notify user, update UI, trigger next workflow step error_log("Payout APPROVED: $payout_order_id | ₹$amount | Type: $withdraw_type"); break; case 'REJECTED': // Notify user with rejection reason from $admin_remarks // Optionally restore user balance in your app error_log("Payout REJECTED: $payout_order_id | Reason: $admin_remarks"); break; default: error_log("Unknown payout status: $payout_status"); break; } // ── Always respond HTTP 200 within 10 seconds ──────────────── http_response_code(200); echo 'ok';
Use this when you want to proactively fetch payout status from your backend — e.g. on a cron job, user refresh, or after a timeout waiting for the automatic callback.
$user_token = 'your_user_token_here'; $payout_order_id = 'PAYOUT_20260302_120000'; // or use payout_id=5 $url = 'https://yayapay.shop/api/payout_callback.php?' . http_build_query([ 'user_token' => $user_token, 'payout_order_id' => $payout_order_id, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if ($response['status'] === true) { $payout_status = $response['payout_status']; // PENDING | APPROVED | REJECTED $withdraw_type = $response['withdraw_type']; echo "Status: $payout_status\n"; echo "Type: $withdraw_type\n"; echo "Amount: ₹" . $response['amount'] . "\n"; if ($withdraw_type === 'usdt') { echo "USDT: " . $response['usdt_amount'] . " to " . $response['trc20_address'] . "\n"; } } else { echo 'Error: ' . $response['message']; }
Your callback receiver must respond HTTP 200 within 10 seconds. The callback is sent once — if your server was down or timed out, use the GET Status endpoint above to manually poll the final status instead.
Callback delivery is logged server-side to ../logs/payout_callbacks.log, recording timestamp, payout ID, target URL, and HTTP response code. Contact support with your payout_order_id if a callback was not received.
Full Working Example
A complete end-to-end example — database schema with sample data, the deposit creation flow, the webhook handler, and the callback receiver. Copy each file into your project and it is ready to test.
Run this SQL once to create all required tables and seed them with test data:
-- ── Processed callbacks (idempotency) ───────────────────────── CREATE TABLE IF NOT EXISTS processed_callbacks ( id INT AUTO_INCREMENT PRIMARY KEY, order_id VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- ── Sample users ────────────────────────────────────────────── INSERT INTO balance (userid, username, balance) VALUES ('USR001', 'Rahul Sharma', 1500.00), ('USR002', 'Priya Singh', 800.50), ('USR003', 'Amit Verma', 0.00); -- ── Sample pending deposit orders ───────────────────────────── INSERT INTO recharge (orderid, balance, userid, status, callback_url) VALUES ('ORD-20260401-001', 500.00, 'USR001', 0, 'https://yoursite.com/payment-callback'), ('ORD-20260401-002', 1000.00, 'USR002', 0, 'https://yoursite.com/payment-callback'), ('ORD-20260401-003', 250.00, 'USR003', 0, 'https://yoursite.com/payment-callback'), ('ORD-20260401-004', 750.00, 'USR001', 1, 'https://yoursite.com/payment-callback'); -- already processed
Call this from your backend when a user clicks "Add Funds". It creates the order in your DB, then calls the YayaPay API to get the payment_url and redirects the user.
<?php // ── DB config ──────────────────────────────────────────────── $conn = new mysqli('localhost', 'dbdetails', 'dbdetails', 'dbdetails'); $conn->set_charset('utf8'); // ── Order details (in real use, get these from session/POST) ── $userId = 'USR001'; $amount = 500; // INR $orderId = 'ORD-' . date('Ymd') . '-' . time(); $callbackUrl = 'https://yoursite.com/payment-callback'; $redirectUrl = 'https://yoursite.com/payment-return?order_id=' . $orderId; $mobile = '9876543210'; // ── Save order to DB BEFORE calling gateway ─────────────────── $stmt = $conn->prepare( "INSERT INTO recharge (orderid, balance, userid, status, callback_url) VALUES (?, ?, ?, '0', ?)" ); $stmt->bind_param('sdss', $orderId, $amount, $userId, $callbackUrl); $stmt->execute(); // ── Call YayaPay API ───────────────────────────────────────── $params = http_build_query([ 'user_token' => 'YOUR_USERTOKEN_HERE', 'customer_mobile' => $mobile, 'amount' => $amount, 'order_id' => $orderId, 'redirect_url' => $redirectUrl, 'remark1' => 'Wallet Deposit', ]); $ch = curl_init('https://yayapay.shop/api/create-order'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $params, CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); // ── Redirect user to payment page ──────────────────────────── if ($response['status'] === true) { header('Location: ' . $response['data']['payment_url']); exit; } else { echo 'Order Error: ' . $response['msg']; }
The gateway POSTs to this file after the user completes payment. It credits the wallet and fires your callback.
<?php define('DB_HOST', 'localhost'); define('DB_USERNAME', 'dbdetails'); define('DB_PASSWORD', 'dbdetails'); define('DB_NAME', 'dbdetails'); define('CALLBACK_SECRET', 'ch@ng3_th1s_s3cr3t'); // must match payment-callback.php $conn = new mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME); if ($conn->connect_error) { die("DB Error: " . $conn->connect_error); } $conn->set_charset('utf8'); function writeLog(string $msg): void { $dir = __DIR__ . '/logs'; if (!file_exists($dir)) mkdir($dir, 0755, true); file_put_contents( $dir . '/log_' . date('Y-m-d') . '.txt', '[' . date('Y-m-d H:i:s') . '] ' . $msg . " ", FILE_APPEND | LOCK_EX ); } function sendCallback(string $url, string $orderId, string $status, float $amount, string $userId, string $remark1): bool { if (empty($url)) { writeLog("No callback_url for $orderId"); return false; } $ts = time(); $sig = hash_hmac('sha256', $orderId.$status.$amount.$ts, CALLBACK_SECRET); $payload = json_encode([ 'order_id' => $orderId, 'status' => $status, 'amount' => $amount, 'user_id' => $userId, 'remark1' => $remark1, 'timestamp' => $ts, 'signature' => $sig, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Webhook-Signature: '.$sig], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, ]); $res = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($err) { writeLog("Callback cURL error [$orderId]: $err"); return false; } writeLog("Callback [$orderId] HTTP $code: " . substr($res, 0, 100)); return ($code >= 200 && $code < 300); } // ── Log raw input ──────────────────────────────────────────── writeLog("IN: " . json_encode(['GET' => $_GET, 'POST' => $_POST])); $order_id = trim($_GET['order_id'] ?? $_POST['order_id'] ?? ''); $status = trim($_GET['status'] ?? $_POST['status'] ?? ''); $remark1 = trim($_GET['remark1'] ?? $_POST['remark1'] ?? ''); if (empty($order_id) || empty($status)) { writeLog("Missing params"); echo json_encode(['status' => false, 'message' => 'Missing parameters']); exit; } else { writeLog("SKIP: $order_id status=$status"); echo "ok1"; } $conn->close(); exit;
Place this at your callback_url. It verifies the signature, blocks replay attacks, prevents double-processing, and lets you act on the result.
<?php $conn = new mysqli('localhost', 'dbdetails', 'dbdetails', 'dbdetails'); $secret = 'ch@ng3_th1s_s3cr3t'; // must match CALLBACK_SECRET in webhook.php $body = file_get_contents('php://input'); $data = json_decode($body, true); if (!$data || !isset($data['order_id'], $data['signature'], $data['timestamp'])) { http_response_code(400); exit('Bad request'); } // 1. Replay-attack check — reject if older than 5 minutes if (abs(time() - (int)$data['timestamp']) > 300) { http_response_code(400); exit('Expired'); } // 2. HMAC signature verification $expected = hash_hmac( 'sha256', $data['order_id'] . $data['status'] . $data['amount'] . $data['timestamp'], $secret ); if (!hash_equals($expected, $data['signature'])) { http_response_code(401); exit('Invalid signature'); } // 3. Idempotency — skip if already processed $stmt = $conn->prepare("SELECT id FROM processed_callbacks WHERE order_id=?"); $stmt->bind_param('s', $data['order_id']); $stmt->execute(); if ($stmt->get_result()->num_rows > 0) { http_response_code(200); exit('ok'); } // 4. ✅ Handle result — add your app logic here if ($data['status'] === 'SUCCESS') { /* * Example actions you can trigger here: * - Send push notification to the user's mobile app * - Update a "pending" UI state to "confirmed" * - Award a first-deposit bonus * - Log the transaction in your own audit table * - Fire a socket event (Pusher, Soketi, etc.) */ error_log("Deposit confirmed: Order {$data['order_id']} | ₹{$data['amount']} → User {$data['user_id']}"); } // 5. Record as processed $stmt = $conn->prepare("INSERT INTO processed_callbacks (order_id) VALUES (?)"); $stmt->bind_param('s', $data['order_id']); $stmt->execute(); http_response_code(200); echo 'ok';
Use cURL to test the webhook locally without a real gateway payment:
# Test SUCCESS — credits ₹500 to USR001 curl -X POST "https://yoursite.com/webhook.php" -d "order_id=ORD-20260401-001&status=SUCCESS&remark1=TestDeposit" # Expected response: ok2 # USR001 balance: 1500.00 → 2000.00 # callback_url receives signed JSON POST automatically # Test already-processed order (idempotency check) curl -X POST "https://yoursite.com/webhook.php" -d "order_id=ORD-20260401-001&status=SUCCESS" # Expected response: ok1 (sthiti already = 1) # Test FAILED status curl -X POST "https://yoursite.com/webhook.php" -d "order_id=ORD-20260401-002&status=FAILED" # Expected response: ok1 (status != SUCCESS, no balance change)
your-project/ ├── schema.sql ← Run once: creates tables + seeds sample data ├── create-order.php ← Step 1: user clicks "Add Funds" → creates order → redirects ├── webhook.php ← Step 2: gateway POSTs here after payment ├── payment-callback.php ← Step 3: your app receives signed callback └── logs/ └── log_YYYY-MM-DD.txt ← Auto-created by writeLog()
Flow summary: User → create-order.php → YayaPay payment page → User pays → Gateway POSTs to webhook.php → Balance credited → payment-callback.php notified with signed payload.
Error Reference
When status is false, read the msg (Pay-In) or message (Pay-Out) field to diagnose the issue.
| Error Message | Reason & Fix |
|---|---|
Missing required fields | One or more required parameters were not sent. Check all required fields. |
withdraw_type must be 'upi', 'bank', or 'usdt' | Invalid value for withdraw_type. Only upi, bank, or usdt accepted. |
Minimum payout amount is ₹100 | The amount is below ₹100 for UPI/Bank. Increase to at least ₹100. |
Minimum payout amount for USDT is ₹118 | USDT payouts require at least ₹118 (= 1 USDT at fixed rate). Increase amount to ₹118 or more. |
Invalid UPI ID | UPI ID format is incorrect. Use username@bank or mobile@upi. |
Invalid account_number | Account number must be 9–18 digits only. |
Invalid IFSC code | IFSC must match format, e.g. HDFC0001234. |
Invalid TRC20 wallet address | TRC20 address must start with T and be exactly 34 Base58 characters. Double-check the wallet address. Example: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE |
Payout Order ID already exists | Duplicate payout_order_id for this user. Use a unique ID per request. |
Insufficient balance | Your balance is less than amount + fee. Top up your merchant account. |
Your Plan Expired. Please Renew | Merchant subscription has expired. Renew your plan in the dashboard. |
Payout API is only available for Unlimited Merchant accounts | Plan upgrade required. Upgrade to Unlimited in the merchant dashboard. |
User not found | Invalid or expired user_token. Re-check your token from the dashboard. |
Never retry a payout if you receive Payout Order ID already exists. The original request may have succeeded — check your dashboard before creating a new payout.
USDT TRC20 note: Always verify the TRC20 wallet address client-side before submitting. Sending USDT to a wrong address is irreversible. The API validates the format but cannot verify address ownership.