Skip to main content

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

FieldTypeDescription
transaction_idStringPaywize generated unique transaction ID
sender_idStringMerchant provided unique sender ID
wallet_idStringMerchant wallet ID
amountStringTransfer amount
payment_modeStringPayment mode used (IMPS, NEFT, RTGS)
remarksStringTransfer description
statusStringCurrent transaction status (INITIATED, PROCESSING, SUCCESS, FAILED, REFUNDED)
status_messageStringDescriptive message about the current status
utr_numberStringUnique Transaction Reference number from bank
beneficiaryObjectBeneficiary information object
beneficiary.beneficiary_nameStringBeneficiary account holder name
beneficiary.beneficiary_acc_numberStringBeneficiary account number
beneficiary.beneficiary_ifscStringBeneficiary bank IFSC code
timestampsObjectTransaction timestamp information
timestamps.created_atStringTransaction creation timestamp (ISO 8601 format)
timestamps.updated_atStringLast update timestamp (ISO 8601 format)

Transaction Status Values

StatusDescriptionWhen Webhook is Sent
PROCESSINGTransaction is being processedWhen payout enters processing
SUCCESSTransaction completed successfullyWhen payout is successful
FAILEDTransaction failedWhen payout fails
REFUNDEDTransaction amount refundedWhen payout is refunded

Implementation Requirements

Response Requirements

Your webhook endpoint MUST:

  1. Respond with HTTP 200 status code
  2. Respond within 30 seconds
  3. Accept POST requests with JSON payload
  4. 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

Learn about Encryption →

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 3000 to 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

  1. Idempotency: Handle duplicate webhooks gracefully using txn_id or client_ref_id
  2. Error Handling: Always respond with proper HTTP status codes
  3. Logging: Log webhook events for debugging but never log decrypted sensitive data
  4. Database Updates: Use transactions when updating multiple database records
  5. Async Processing: For heavy processing, respond quickly and process asynchronously
  6. Monitoring: Monitor webhook endpoint uptime and response times
  7. Status-specific Logic: Implement different logic for each status type
  8. Backup Processing: Implement fallback status checking if webhooks fail

Troubleshooting

Common Issues

IssueSolution
Webhook not receivedCheck URL configuration and server accessibility
500 Internal Server ErrorCheck server logs and fix code errors
TimeoutEnsure response time is under 30 seconds
Duplicate processingImplement idempotency using txn_id
Decryption failureVerify 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 });
});

Next Steps