PayU Merchant Status Webhooks
Receive real-time notifications for document verification, bank validation, KYC approvals, and other merchant onboarding milestones.
Getting Started
Prerequisites & Setup
Before integrating webhooks, you'll need:
Important: Contact PayU support or your Key Account Manager to enable real-time merchant status service for your reseller account.
Required Items:
- ✅ PayU partner credentials with
refer_merchantscope - ✅ Your reseller UUID
- ✅ Client secret for HMAC verification
- ✅ HTTPS endpoint that returns 200 status codes
- ✅ Webhook service enabled by PayU support
Quick Check:
# Test your endpoint accessibility
curl -X POST https://your-domain.com/payu/webhooks \
-H "Content-Type: application/json" \
-d '{"test": "connectivity"}'
# Should return: 200 OKRegister Your Webhook
API Registration
Register a single HTTPS endpoint to receive all merchant status events.
Endpoint: POST /api/v1/partners/register_webhook
curl https://partner.payu.in/api/v1/partners/register_webhook \
-H "Authorization: Bearer your_access_token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d webhook_url=https://your-domain.com/payu/webhooks \
-d reseller_uuid=83fe-eb64-021844d8-9397-26535b1bf0c2Parameters:
| Field | Description |
|---|---|
webhook_url | Your HTTPS endpoint URL |
reseller_uuid | Your PayU reseller identifier |
Success Response:
{
"message": "Webhook Successfully Registered"
}Understanding Webhook Events
Event Types & Payloads
PayU sends POST requests when merchant status changes occur during onboarding.
Sample Webhook Request:
{
"previous_status": "Pending",
"current_status": "Approved",
"change_timestamp": 1654812374,
"mid": 123456,
"merchant_uuid": "123-abcd-5678-gcjsa",
"event_name": "Document status update",
"error": "NA",
"remarks": "NA"
}Merchant Onboarding Events:
🏢 Business Events
- • Document status update
- • Website status update
- • Agreement status update
- • Settlement status update
Status Values:
Implement Webhook Security
HMAC Signature Verification
Every webhook includes an HMAC signature in the Authorization header. Always validate this to ensure authenticity.
PayU's Signature Process:
- Sort payload keys alphabetically
- Concatenate key-value pairs:
"key1value1key2value2..." - Generate HMAC-SHA256 using your
client_secret - Send hex digest in Authorization header
Example Calculation:
{`{
"previous_status": "Pending",
"current_status": "Success",
"change_timestamp": 18548123746,
"mid": 123456,
"merchant_uuid": "123-abcd-5678-gcjsa",
"event_name": "Document status update",
"error": "NA",
"remarks": "NA"
}`}
change_timestamp18548123746current_statusSuccesserrorNAevent_nameDocument status updatemerchant_uuid123-abcd-5678-gcjsamid123456previous_statusPendingremarksNA
Implementation:
Python
import hmac
import hashlib
def verify_signature(payload, signature, client_secret):
# Create sorted string
items = [f"{k}{v}" for k, v in sorted(payload.items())]
payload_string = "".join(items)
# Generate expected signature
expected = hmac.new(
client_secret.encode('utf-8'),
payload_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, clientSecret) {
// Create sorted string
const keys = Object.keys(payload).sort();
const payloadString = keys.map(k => `${k}${payload[k]}`).join('');
// Generate expected signature
const expected = crypto
.createHmac('sha256', clientSecret)
.update(payloadString)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}Build Your Webhook Endpoint
Event Processing Implementation
Create a robust webhook handler that processes merchant status updates.
Basic Webhook Handler:
from flask import Flask, request
import logging
app = Flask(__name__)
CLIENT_SECRET = "your_client_secret" # Store securely
@app.route('/payu/webhooks', methods=['POST'])
def handle_webhook():
try:
payload = request.get_json()
signature = request.headers.get('Authorization', '')
# Verify signature first
if not verify_signature(payload, signature, CLIENT_SECRET):
logging.warning('Invalid webhook signature')
return '', 200 # Return 200 to prevent retries
# Process the merchant event
process_merchant_event(payload)
return '', 200
except Exception as e:
logging.error(f"Webhook error: {e}")
return '', 200 # Always return 200Event Routing Logic:
def process_merchant_event(payload):
event_name = payload['event_name']
merchant_uuid = payload['merchant_uuid']
status = payload['current_status']
# Route based on event type
if event_name == 'Bank verification status update':
handle_bank_verification(merchant_uuid, status, payload)
elif event_name == 'Settlement status update':
handle_settlement_update(merchant_uuid, status, payload)
elif any(kyc in event_name for kyc in ['SIGNED_', 'PAN', 'GOVT_', 'BANK_PROOF', 'ADDRESS_']):
handle_kyc_document(merchant_uuid, event_name, status, payload)
else:
handle_general_status_update(merchant_uuid, event_name, status, payload)
def handle_bank_verification(merchant_uuid, status, payload):
if status == 'Approved':
# Enable payment processing
enable_merchant_payments(merchant_uuid)
send_approval_notification(merchant_uuid, 'bank_verification')
elif status == 'Declined':
# Handle rejection
error_details = payload.get('error', 'Unknown error')
notify_bank_verification_failure(merchant_uuid, error_details)
def handle_kyc_document(merchant_uuid, doc_type, status, payload):
# Update document status in your system
update_document_status(merchant_uuid, doc_type, status)
if status == 'Approved':
# Check if all KYC documents are now complete
if all_kyc_documents_approved(merchant_uuid):
complete_kyc_process(merchant_uuid)
elif status == 'Declined':
# Notify merchant with specific feedback
error_reason = payload.get('error', 'Document verification failed')
request_document_resubmission(merchant_uuid, doc_type, error_reason)Idempotency Handling:
# Use database or Redis in production
processed_events = set()
def is_duplicate_event(payload):
event_id = f"{payload['merchant_uuid']}_{payload['change_timestamp']}_{payload['event_name']}"
if event_id in processed_events:
return True
processed_events.add(event_id)
return False
@app.route('/payu/webhooks', methods=['POST'])
def handle_webhook():
payload = request.get_json()
# Check for duplicates first
if is_duplicate_event(payload):
logging.info('Duplicate webhook ignored')
return '', 200
# Continue with processing...Error Handling & Retries
PayU implements automatic retries for failed webhook deliveries.
PayU Retry Policy:
Retry Schedule: 5 attempts at 3, 9, 27, 81, 243 seconds
Your endpoint must: Return 200 status, respond within 30 seconds, handle duplicates
Robust Error Handling:
import logging
from datetime import datetime
@app.route('/payu/webhooks', methods=['POST'])
def handle_webhook():
start_time = datetime.utcnow()
webhook_id = None
try:
payload = request.get_json()
webhook_id = f"{payload.get('merchant_uuid')}_{payload.get('change_timestamp')}"
logging.info(f"Processing webhook: {webhook_id}")
# Validate payload structure
if not all(key in payload for key in ['merchant_uuid', 'event_name', 'current_status']):
logging.warning(f"Invalid payload structure: {webhook_id}")
return '', 200
# Verify signature
if not verify_signature(payload, request.headers.get('Authorization'), CLIENT_SECRET):
logging.warning(f"Invalid signature: {webhook_id}")
return '', 200
# Process webhook
process_merchant_event(payload)
# Log success
duration = (datetime.utcnow() - start_time).total_seconds()
logging.info(f"Webhook processed successfully: {webhook_id} ({duration:.2f}s)")
return '', 200
except Exception as e:
# Log error with context but always return 200
duration = (datetime.utcnow() - start_time).total_seconds()
logging.error(f"Webhook processing failed: {webhook_id} - {e} ({duration:.2f}s)")
# Optional: Queue for manual review or retry
queue_failed_webhook(payload, str(e))
return '', 200 # Prevent PayU retriesProduction Monitoring:
# Track webhook metrics
webhook_stats = {
'total_received': 0,
'successful': 0,
'failed': 0,
'invalid_signatures': 0
}
def track_webhook_result(result_type):
webhook_stats['total_received'] += 1
webhook_stats[result_type] += 1
# Alert on high failure rate
failure_rate = webhook_stats['failed'] / webhook_stats['total_received']
if failure_rate > 0.1: # 10% threshold
send_alert(f"High webhook failure rate: {failure_rate:.1%}")Testing Your Integration
Development & Testing Tools
Test your webhook implementation before production deployment.
Local Development Setup:
# Use ngrok to expose local development server
ngrok http 3000
# Register your ngrok URL with PayU
curl https://uat-partner.payu.in/api/v1/partners/register_webhook \
-H "Authorization: Bearer your_test_token" \
-d webhook_url=https://abc123.ngrok.io/payu/webhooks \
-d reseller_uuid=your_test_uuidWebhook Simulation Script:
import requests
import hmac
import hashlib
import time
def simulate_payu_webhook(webhook_url, client_secret, event_type="document_approval"):
# Sample payloads for different scenarios
scenarios = {
"document_approval": {
"previous_status": "Pending",
"current_status": "Approved",
"event_name": "Document status update",
"error": "NA",
"remarks": "NA"
},
"bank_verification": {
"previous_status": "Pending",
"current_status": "Approved",
"event_name": "Bank verification status update",
"error": "NA",
"remarks": "NA"
},
"kyc_declined": {
"previous_status": "Received",
"current_status": "Declined",
"event_name": "SIGNED_AUTHORISATION_LETTER status update",
"error": "Document clarity insufficient",
"remarks": "Please resubmit with better quality"
}
}
payload = {
**scenarios.get(event_type, scenarios["document_approval"]),
"change_timestamp": int(time.time()),
"mid": 123456,
"merchant_uuid": "test-merchant-001"
}
# Generate PayU signature
sorted_items = [f"{k}{v}" for k, v in sorted(payload.items())]
payload_string = "".join(sorted_items)
signature = hmac.new(
client_secret.encode('utf-8'),
payload_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Send webhook
response = requests.post(
webhook_url,
json=payload,
headers={'Authorization': signature}
)
print(f"Test '{event_type}': {response.status_code}")
return response.status_code == 200
# Run tests
webhook_url = "https://your-domain.com/payu/webhooks"
client_secret = "your_test_client_secret"
print("Testing PayU webhooks...")
simulate_payu_webhook(webhook_url, client_secret, "document_approval")
simulate_payu_webhook(webhook_url, client_secret, "bank_verification")
simulate_payu_webhook(webhook_url, client_secret, "kyc_declined")Validation Checklist:
Production Deployment
Go-Live Checklist
Final steps before deploying your webhook integration to production.
Security Verification:
Production Registration:
# Register your production webhook URL
curl https://partner.payu.in/api/v1/partners/register_webhook \
-H "Authorization: Bearer your_production_token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d webhook_url=https://your-production-domain.com/payu/webhooks \
-d reseller_uuid=your_production_reseller_uuidPost-Deployment Monitoring:
# Health check endpoint for monitoring
@app.route('/payu/webhooks/health', methods=['GET'])
def webhook_health():
recent_errors = get_recent_webhook_errors() # Last 24 hours
status = "healthy" if len(recent_errors) < 10 else "degraded"
return {
"status": status,
"webhook_stats": webhook_stats,
"recent_errors": len(recent_errors),
"last_webhook": get_last_webhook_timestamp()
}Launch Strategy:
- Deploy webhook endpoint to production
- Register production URL with PayU
- Monitor first few webhook deliveries closely
- Validate merchant status updates in your system
- Confirm alerting and monitoring are working
Troubleshooting
Common Issues & Solutions
Quick solutions for frequently encountered webhook problems.
❌ Not Receiving Webhooks
Check: Webhook service enabled by PayU support
Verify: Endpoint returns 200 and is publicly accessible
Test: curl -X POST
⚠️ Signature Validation Fails
Verify: Using correct client\_secret from PayU dashboard
Check: Payload keys sorted alphabetically before concatenation
Ensure: Using HMAC-SHA256 algorithm exactly
Debug Script:
# Quick webhook endpoint test
curl -X POST https://your-domain.com/payu/webhooks \
-H "Content-Type: application/json" \
-H "Authorization: test_signature" \
-d '{
"merchant_uuid": "test-123",
"event_name": "Document status update",
"current_status": "Approved",
"change_timestamp": 1654812374,
"mid": 123456,
"previous_status": "Pending",
"error": "NA",
"remarks": "NA"
}'Support & Resources
📞 Get Help
Enable Webhooks: Contact PayU Key Account Manager
Technical Support: [email protected]
KYC Issues: KYC Errors & Solutions
