Payout Webhooks
Configure webhooks for real-time payout status notifications.
Last updated: 2026-02-21
Webhook Notifications
Overview
Paywize sends payout status updates to your webhook URL whenever a transaction status changes. This provides real-time notifications without the need for constant polling of the status endpoint.
Webhook Configuration
Webhooks are sent to the callback_url provided during payout initiation via the Payout Initiate API. Each payout can have its own webhook URL by specifying a different callback_url in the request.
Required Field in Payout Request:
{
"callback_url": "https://your-website.com/webhook/payout"
}
Webhook Delivery: Paywize will send status updates to the specified callback_url whenever the transaction status changes.
Request Headers (From Paywize)
When Paywize sends webhooks to your endpoint, the following headers are included:
Content-Type: application/json
User-Agent: PayWize-Webhook/1.0
X-Paywize-Signature: sha256=signature_hash
Webhook Payload
Encrypted Payload Example
{
"data": "Vv9KKQofE6eVVpVtWbEMlRUUeMpXnQ3T3OwD3I4iStD0u85Ntbgv35S6vY8rNb3v7mFW6j2s6gnKA44saJwYOyj4rM1BWXo6TWPsNRpyz40Og1w"
}
Decrypted Payload Example
{
"transaction_id": "PAY123456789",
"sender_id": "unique_transaction_id",
"wallet_id": "virtual_account_number",
"amount": "1000.00",
"payment_mode": "IMPS",
"remarks": "Payment",
"status": "SUCCESS",
"status_message": "Payment completed successfully",
"utr_number": "415612345678",
"beneficiary": {
"beneficiary_name": "John Doe",
"beneficiary_acc_number": "123456789012",
"beneficiary_ifsc": "HDFC0001234"
},
"timestamps": {
"created_at": "2025-11-05T12:30:15Z",
"updated_at": "2025-11-05T12:35:22Z"
}
}
Webhook Payload Fields
| Field | Type | Description |
|---|---|---|
| transaction_id | String | Paywize generated unique transaction ID |
| sender_id | String | Merchant provided unique sender ID |
| wallet_id | String | Merchant wallet ID |
| amount | String | Transfer amount |
| payment_mode | String | Payment mode used (IMPS, NEFT, RTGS) |
| remarks | String | Transfer description |
| status | String | Current transaction status (INITIATED, PROCESSING, SUCCESS, FAILED, REFUNDED) |
| status_message | String | Descriptive message about the current status |
| utr_number | String | Unique Transaction Reference number from bank |
| beneficiary | Object | Beneficiary information object |
| beneficiary.beneficiary_name | String | Beneficiary account holder name |
| beneficiary.beneficiary_acc_number | String | Beneficiary account number |
| beneficiary.beneficiary_ifsc | String | Beneficiary bank IFSC code |
| timestamps | Object | Transaction timestamp information |
| timestamps.created_at | String | Transaction creation timestamp (ISO 8601 format) |
| timestamps.updated_at | String | Last update timestamp (ISO 8601 format) |
Transaction Status Values
| Status | Description | When Webhook is Sent |
|---|---|---|
| PROCESSING | Transaction is being processed | When payout enters processing |
| SUCCESS | Transaction completed successfully | When payout is successful |
| FAILED | Transaction failed | When payout fails |
| REFUNDED | Transaction amount refunded | When payout is refunded |
Implementation Requirements
Response Requirements
Your webhook endpoint MUST:
- Respond with HTTP 200 status code
- Respond within 30 seconds
- Accept POST requests with JSON payload
- Decrypt the payload using your API credentials
Retry Logic
If your webhook endpoint fails to respond properly:
- Retry Attempts: Up to 3 times
- Retry Interval: Exponential backoff (1s, 2s, 4s)
- Timeout: 30 seconds per attempt
Implementation Examples
JavaScript/Node.js
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
// Decryption function
function decryptMerchantData(data, key, iv) {
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
const decrypted = Buffer.concat([
decipher.update(Buffer.from(data, 'base64')),
decipher.final()
]);
return decrypted.toString('utf8');
}
// Webhook endpoint
app.post('/webhook/paywize/payout', (req, res) => {
try {
const { data } = req.body;
if (!data) {
return res.status(400).json({ error: 'Missing webhook data' });
}
// Decrypt the webhook payload
const decryptedData = decryptMerchantData(data, API_KEY, SECRET_KEY);
const payoutUpdate = JSON.parse(decryptedData);
console.log('Payout update received:', payoutUpdate);
// Process the payout update
processPayoutUpdate(payoutUpdate);
// Respond with 200 OK
res.status(200).json({
status: 'success',
message: 'Webhook processed successfully'
});
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({
status: 'error',
message: 'Failed to process webhook'
});
}
});
function processPayoutUpdate(payoutData) {
const { txn_id, status, client_ref_id, utr, beneficiary } = payoutData;
switch (status) {
case 'SUCCESS':
console.log(`Payout ${txn_id} completed successfully with UTR: ${utr}`);
// Update your database, send confirmation emails, etc.
updatePayoutStatus(client_ref_id, 'completed', payoutData);
sendSuccessNotification(beneficiary, payoutData);
break;
case 'FAILED':
console.log(`Payout ${txn_id} failed`);
// Handle failed payout, notify user, credit back amount, etc.
updatePayoutStatus(client_ref_id, 'failed', payoutData);
handleFailedPayout(client_ref_id, payoutData);
break;
case 'PENDING':
console.log(`Payout ${txn_id} is pending review`);
// Handle pending status, notify admin for review
updatePayoutStatus(client_ref_id, 'pending', payoutData);
notifyAdminForReview(payoutData);
break;
case 'PROCESSING':
console.log(`Payout ${txn_id} is being processed`);
// Update status to processing
updatePayoutStatus(client_ref_id, 'processing', payoutData);
break;
case 'INITIATED':
console.log(`Payout ${txn_id} has been initiated`);
// Payout initiated successfully
updatePayoutStatus(client_ref_id, 'initiated', payoutData);
break;
case 'REFUNDED':
console.log(`Payout ${txn_id} has been refunded`);
// Handle refund, credit amount back to wallet
updatePayoutStatus(client_ref_id, 'refunded', payoutData);
processRefund(client_ref_id, payoutData);
break;
default:
console.log(`Unknown status ${status} for payout ${txn_id}`);
}
}
function updatePayoutStatus(clientRefId, status, payoutData) {
// Your database update logic here
console.log(`Updating payout ${clientRefId} to status: ${status}`);
}
function sendSuccessNotification(beneficiary, payoutData) {
// Send email/SMS notification to beneficiary
console.log(`Sending success notification to ${beneficiary.name}`);
}
function handleFailedPayout(clientRefId, payoutData) {
// Handle failed payout - credit back to wallet, notify user
console.log(`Handling failed payout for ${clientRefId}`);
}
function notifyAdminForReview(payoutData) {
// Notify admin for manual review
console.log(`Notifying admin for review of payout ${payoutData.txn_id}`);
}
function processRefund(clientRefId, payoutData) {
// Process refund - credit amount back to user account
console.log(`Processing refund for ${clientRefId}`);
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
Python (Flask)
from flask import Flask, request, jsonify
import json
from encryption_utils import decrypt_merchant_data
app = Flask(__name__)
@app.route('/webhook/paywize/payout', methods=['POST'])
def handle_webhook():
try:
data = request.json.get('data')
if not data:
return jsonify({'error': 'Missing webhook data'}), 400
# Decrypt the webhook payload
decrypted_data = decrypt_merchant_data(data, API_KEY, SECRET_KEY)
payout_update = json.loads(decrypted_data)
print('Payout update received:', payout_update)
# Process the payout update
process_payout_update(payout_update)
# Respond with 200 OK
return jsonify({
'status': 'success',
'message': 'Webhook processed successfully'
}), 200
except Exception as error:
print('Webhook processing error:', str(error))
return jsonify({
'status': 'error',
'message': 'Failed to process webhook'
}), 500
def process_payout_update(payout_data):
txn_id = payout_data.get('txn_id')
status = payout_data.get('status')
client_ref_id = payout_data.get('client_ref_id')
utr = payout_data.get('utr')
if status == 'SUCCESS':
print(f'Payout {txn_id} completed successfully with UTR: {utr}')
update_payout_status(client_ref_id, 'completed', payout_data)
send_success_notification(payout_data)
elif status == 'FAILED':
print(f'Payout {txn_id} failed')
update_payout_status(client_ref_id, 'failed', payout_data)
handle_failed_payout(client_ref_id, payout_data)
elif status == 'PENDING':
print(f'Payout {txn_id} is pending review')
update_payout_status(client_ref_id, 'pending', payout_data)
notify_admin_for_review(payout_data)
elif status == 'PROCESSING':
print(f'Payout {txn_id} is being processed')
update_payout_status(client_ref_id, 'processing', payout_data)
elif status == 'INITIATED':
print(f'Payout {txn_id} has been initiated')
update_payout_status(client_ref_id, 'initiated', payout_data)
elif status == 'REFUNDED':
print(f'Payout {txn_id} has been refunded')
update_payout_status(client_ref_id, 'refunded', payout_data)
process_refund(client_ref_id, payout_data)
else:
print(f'Unknown status {status} for payout {txn_id}')
def update_payout_status(client_ref_id, status, payout_data):
# Your database update logic here
print(f'Updating payout {client_ref_id} to status: {status}')
def send_success_notification(payout_data):
# Send notification logic
print(f'Sending success notification for {payout_data["txn_id"]}')
def handle_failed_payout(client_ref_id, payout_data):
# Handle failed payout logic
print(f'Handling failed payout for {client_ref_id}')
def notify_admin_for_review(payout_data):
# Notify admin logic
print(f'Notifying admin for review of payout {payout_data["txn_id"]}')
def process_refund(client_ref_id, payout_data):
# Process refund logic
print(f'Processing refund for {client_ref_id}')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000, debug=True)
PHP
<?php
require_once 'PaywizeEncryption.php';
// Get the webhook payload
$input = file_get_contents('php://input');
$webhookData = json_decode($input, true);
// Set content type
header('Content-Type: application/json');
try {
if (!isset($webhookData['data'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing webhook data']);
exit;
}
// Decrypt the webhook payload
$decryptedData = PaywizeEncryption::decryptMerchantData(
$webhookData['data'],
$apiKey,
$secretKey
);
$payoutUpdate = json_decode($decryptedData, true);
error_log('Payout update received: ' . print_r($payoutUpdate, true));
// Process the payout update
processPayoutUpdate($payoutUpdate);
// Respond with 200 OK
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => 'Webhook processed successfully'
]);
} catch (Exception $error) {
error_log('Webhook processing error: ' . $error->getMessage());
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => 'Failed to process webhook'
]);
}
function processPayoutUpdate($payoutData) {
$txnId = $payoutData['txn_id'];
$status = $payoutData['status'];
$clientRefId = $payoutData['client_ref_id'];
$utr = $payoutData['utr'] ?? null;
switch ($status) {
case 'SUCCESS':
error_log("Payout $txnId completed successfully with UTR: $utr");
updatePayoutStatus($clientRefId, 'completed', $payoutData);
sendSuccessNotification($payoutData);
break;
case 'FAILED':
error_log("Payout $txnId failed");
updatePayoutStatus($clientRefId, 'failed', $payoutData);
handleFailedPayout($clientRefId, $payoutData);
break;
case 'PENDING':
error_log("Payout $txnId is pending review");
updatePayoutStatus($clientRefId, 'pending', $payoutData);
notifyAdminForReview($payoutData);
break;
case 'PROCESSING':
error_log("Payout $txnId is being processed");
updatePayoutStatus($clientRefId, 'processing', $payoutData);
break;
case 'INITIATED':
error_log("Payout $txnId has been initiated");
updatePayoutStatus($clientRefId, 'initiated', $payoutData);
break;
case 'REFUNDED':
error_log("Payout $txnId has been refunded");
updatePayoutStatus($clientRefId, 'refunded', $payoutData);
processRefund($clientRefId, $payoutData);
break;
default:
error_log("Unknown status $status for payout $txnId");
}
}
function updatePayoutStatus($clientRefId, $status, $payoutData) {
// Your database update logic here
error_log("Updating payout $clientRefId to status: $status");
}
function sendSuccessNotification($payoutData) {
// Send notification logic
error_log("Sending success notification for {$payoutData['txn_id']}");
}
function handleFailedPayout($clientRefId, $payoutData) {
// Handle failed payout logic
error_log("Handling failed payout for $clientRefId");
}
function notifyAdminForReview($payoutData) {
// Notify admin logic
error_log("Notifying admin for review of payout {$payoutData['txn_id']}");
}
function processRefund($clientRefId, $payoutData) {
// Process refund logic
error_log("Processing refund for $clientRefId");
}
?>
Security Considerations
Data Encryption
All webhook payloads are encrypted using AES-256-CBC encryption:
- Use your API Key as the encryption key
- Use your Secret Key as the initialization vector
- Decrypt payload using the same credentials used for API requests
IP Whitelisting
Ensure your webhook endpoint accepts requests only from Paywize IP addresses. Contact support for the current IP whitelist.
HTTPS Only
Always use HTTPS for webhook URLs to ensure data security in transit.
Testing Webhooks
Local Development
For local testing, use tools like:
- ngrok:
ngrok http 3000to expose local webhook endpoint - webhook.site: For quick webhook testing
- Postman: To simulate webhook payloads
Test Webhook Payload
{
"data": "test_encrypted_payout_payload_here"
}
Best Practices
- Idempotency: Handle duplicate webhooks gracefully using txn_id or client_ref_id
- Error Handling: Always respond with proper HTTP status codes
- Logging: Log webhook events for debugging but never log decrypted sensitive data
- Database Updates: Use transactions when updating multiple database records
- Async Processing: For heavy processing, respond quickly and process asynchronously
- Monitoring: Monitor webhook endpoint uptime and response times
- Status-specific Logic: Implement different logic for each status type
- Backup Processing: Implement fallback status checking if webhooks fail
Troubleshooting
Common Issues
| Issue | Solution |
|---|---|
| Webhook not received | Check URL configuration and server accessibility |
| 500 Internal Server Error | Check server logs and fix code errors |
| Timeout | Ensure response time is under 30 seconds |
| Duplicate processing | Implement idempotency using txn_id |
| Decryption failure | Verify API Key and Secret Key are correct |
Debug Webhook
app.post('/webhook/paywize/payout', (req, res) => {
console.log('Headers:', req.headers);
console.log('Body:', req.body);
// Your webhook processing logic
res.status(200).json({ received: true });
});