Skip to main content

Collection Webhooks

Configure webhooks for real-time collection notifications.

Last updated: 2026-02-21

Webhook Notifications

Overview

Paywize sends payment status updates to your webhook URL whenever a payment status changes. This provides real-time notifications without the need for constant polling of the status endpoint.

Webhook Endpoint

POST https://merchant.paywize.in/api/collection/v1/webhook

Description: Paywize sends payment status updates to the callbackUrl specified during payment initiation when payment status changes.

Webhook Configuration

Webhooks are automatically sent to the callbackUrl you provide when initiating a payment via the /collection/v1/initiate/ API. No separate configuration is needed.

How It Works

  1. When you call /collection/v1/initiate/, include a callbackUrl in your request
  2. Paywize stores this URL for the specific payment transaction
  3. When the payment status changes, Paywize sends a webhook to your callbackUrl
  4. Your endpoint receives the encrypted webhook payload

Example:

// When initiating payment, specify your webhook URL
const paymentData = {
  senderId: "TXN123456",
  txnType: "INTENT",
  requestAmount: "100.50",
  callbackUrl: "https://your-website.com/webhook/collection" // ← This URL receives webhooks
};

Requirements

Ensure your callbackUrl is:

  • Accessible: From the internet (not localhost for production)
  • Method: Configured to accept POST requests
  • Response Time: Responds within 30 seconds with HTTP 200
  • HTTPS: Uses secure HTTPS protocol (recommended)

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

Webhook Payload

Encrypted Payload Example

{
  "data": "Vv9KKQofE6eVVpVtWbEMlRUUeMpXnQ3T3OwD3I4iStD0u85Ntbgv35S6vY8rNb3v7mFW6j2s6gnKA44saJwYOyj4rM1BWXo6TWPsNRpyz40Og1w"
}

Decrypted Payload Example

{
  "senderId": "SENDERID00001",
  "txnId": "CFCE160825000001",
  "requestAmount": "100.00",
  "paymentMode": "Intent",
  "utr": "405812345678",
  "remarks": "test",
  "status": "SUCCESS",
  "statusMessage": "Payment completed successfully",
  "createdAt": "2025-08-16T07:46:45.277Z",
  "updatedAt": "2025-08-16T08:18:15.378Z"
}

Webhook Payload Fields

FieldDescription
senderIdMerchant provided unique sender ID
txnIdPaywize generated unique transaction ID
requestAmountPayment amount requested
paymentModePayment method used (Intent, etc.)
utrUnique Transaction Reference from bank (available for successful payments)
remarksPayment remarks/description
statusCurrent transaction status
statusMessageDetailed status description
createdAtTransaction creation timestamp
updatedAtLast update timestamp

Transaction Status Values

StatusDescriptionWhen Webhook is Sent
INITIATEDPayment request created, awaiting customer actionWhen payment link is generated
SUCCESSPayment completed successfullyWhen payment is successful
FAILEDPayment failedWhen payment fails
PENDINGPayment under reviewWhen payment is under review

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/collection', (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 paymentUpdate = JSON.parse(decryptedData);

    console.log('Payment update received:', paymentUpdate);

    // Process the payment update
    processPaymentUpdate(paymentUpdate);

    // 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 processPaymentUpdate(paymentData) {
  const { txnId, status, senderId, utr } = paymentData;

  switch (status) {
    case 'SUCCESS':
      console.log(`Payment ${txnId} completed successfully with UTR: ${utr}`);
      // Update your database, send confirmation emails, etc.
      updatePaymentStatus(senderId, 'completed', paymentData);
      break;

    case 'FAILED':
      console.log(`Payment ${txnId} failed`);
      // Handle failed payment, notify user, etc.
      updatePaymentStatus(senderId, 'failed', paymentData);
      break;

    case 'PENDING':
      console.log(`Payment ${txnId} is pending review`);
      // Handle pending status
      updatePaymentStatus(senderId, 'pending', paymentData);
      break;

    case 'INITIATED':
      console.log(`Payment ${txnId} has been initiated`);
      // Payment link generated, ready for customer
      updatePaymentStatus(senderId, 'initiated', paymentData);
      break;

    default:
      console.log(`Unknown status ${status} for payment ${txnId}`);
  }
}

function updatePaymentStatus(senderId, status, paymentData) {
  // Your database update logic here
  console.log(`Updating payment ${senderId} to status: ${status}`);
}

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/collection', 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)
        payment_update = json.loads(decrypted_data)

        print('Payment update received:', payment_update)

        # Process the payment update
        process_payment_update(payment_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_payment_update(payment_data):
    txn_id = payment_data.get('txnId')
    status = payment_data.get('status')
    sender_id = payment_data.get('senderId')
    utr = payment_data.get('utr')

    if status == 'SUCCESS':
        print(f'Payment {txn_id} completed successfully with UTR: {utr}')
        # Update your database, send confirmation emails, etc.
        update_payment_status(sender_id, 'completed', payment_data)

    elif status == 'FAILED':
        print(f'Payment {txn_id} failed')
        # Handle failed payment, notify user, etc.
        update_payment_status(sender_id, 'failed', payment_data)

    elif status == 'PENDING':
        print(f'Payment {txn_id} is pending review')
        # Handle pending status
        update_payment_status(sender_id, 'pending', payment_data)

    elif status == 'INITIATED':
        print(f'Payment {txn_id} has been initiated')
        # Payment link generated, ready for customer
        update_payment_status(sender_id, 'initiated', payment_data)

    else:
        print(f'Unknown status {status} for payment {txn_id}')

def update_payment_status(sender_id, status, payment_data):
    # Your database update logic here
    print(f'Updating payment {sender_id} to status: {status}')

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

    $paymentUpdate = json_decode($decryptedData, true);

    error_log('Payment update received: ' . print_r($paymentUpdate, true));

    // Process the payment update
    processPaymentUpdate($paymentUpdate);

    // 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 processPaymentUpdate($paymentData) {
    $txnId = $paymentData['txnId'];
    $status = $paymentData['status'];
    $senderId = $paymentData['senderId'];
    $utr = $paymentData['utr'] ?? null;

    switch ($status) {
        case 'SUCCESS':
            error_log("Payment $txnId completed successfully with UTR: $utr");
            // Update your database, send confirmation emails, etc.
            updatePaymentStatus($senderId, 'completed', $paymentData);
            break;

        case 'FAILED':
            error_log("Payment $txnId failed");
            // Handle failed payment, notify user, etc.
            updatePaymentStatus($senderId, 'failed', $paymentData);
            break;

        case 'PENDING':
            error_log("Payment $txnId is pending review");
            // Handle pending status
            updatePaymentStatus($senderId, 'pending', $paymentData);
            break;

        case 'INITIATED':
            error_log("Payment $txnId has been initiated");
            // Payment link generated, ready for customer
            updatePaymentStatus($senderId, 'initiated', $paymentData);
            break;

        default:
            error_log("Unknown status $status for payment $txnId");
    }
}

function updatePaymentStatus($senderId, $status, $paymentData) {
    // Your database update logic here
    error_log("Updating payment $senderId to status: $status");
}

?>

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_payload_here"
}

Webhook Verification

Verify Webhook Authenticity

function verifyWebhook(webhookData, apiKey, secretKey) {
  try {
    // Attempt to decrypt the payload
    const decryptedData = decryptMerchantData(webhookData.data, apiKey, secretKey);
    const paymentData = JSON.parse(decryptedData);

    // Verify required fields exist
    const requiredFields = ['txnId', 'senderId', 'status', 'statusMessage'];
    const isValid = requiredFields.every(field => paymentData.hasOwnProperty(field));

    return isValid;
  } catch (error) {
    console.error('Webhook verification failed:', error);
    return false;
  }
}

Best Practices

  1. Idempotency: Handle duplicate webhooks gracefully using txnId or senderId
  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. 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 txnId
Decryption failureVerify API Key and Secret Key are correct

Debug Webhook

app.post('/webhook/paywize/collection', (req, res) => {
  console.log('Headers:', req.headers);
  console.log('Body:', req.body);

  // Your webhook processing logic

  res.status(200).json({ received: true });
});

Next Steps