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
- When you call
/collection/v1/initiate/, include acallbackUrlin your request - Paywize stores this URL for the specific payment transaction
- When the payment status changes, Paywize sends a webhook to your
callbackUrl - 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
| Field | Description |
|---|---|
| senderId | Merchant provided unique sender ID |
| txnId | Paywize generated unique transaction ID |
| requestAmount | Payment amount requested |
| paymentMode | Payment method used (Intent, etc.) |
| utr | Unique Transaction Reference from bank (available for successful payments) |
| remarks | Payment remarks/description |
| status | Current transaction status |
| statusMessage | Detailed status description |
| createdAt | Transaction creation timestamp |
| updatedAt | Last update timestamp |
Transaction Status Values
| Status | Description | When Webhook is Sent |
|---|---|---|
| INITIATED | Payment request created, awaiting customer action | When payment link is generated |
| SUCCESS | Payment completed successfully | When payment is successful |
| FAILED | Payment failed | When payment fails |
| PENDING | Payment under review | When payment is under review |
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/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
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_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
- Idempotency: Handle duplicate webhooks gracefully using txnId or senderId
- 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
- 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 txnId |
| Decryption failure | Verify 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 });
});