Webhooks Overview
Receive real-time notifications when payments are completed, failed, or refunded
What are Webhooks?
Webhooks allow Payzo to send real-time notifications to your server when payment events occur. This is the recommended way to track payment status instead of polling the API.
Why Use Webhooks?
Real-time notifications - Get instant updates when payments complete Reliable - Automatic retries with exponential backoff Efficient - No need to poll the API repeatedly Secure - HMAC signature verification
Setting Up Webhooks
Step 1: Create a Webhook Endpoint
Create an endpoint on your server to receive webhook events:
// Node.js / Express
app.post('/webhook/payzo', async (req, res) => {
const { event, payment, shop } = req.body;
// Always respond quickly (within 10 seconds)
res.status(200).json({ received: true });
// Process the webhook asynchronously
if (event === 'payment.completed') {
await processPaymentCompleted(payment);
}
});
Step 2: Configure Webhook URL in Dashboard
- Go to your Dashboard
- Navigate to Shops
- Select your shop
- Click Edit or Settings
- Enter your webhook URL:
https://yoursite.com/webhook/payzo - Click Save
You can configure up to 2 webhook URLs per shop (webhook_url and webhook_url_2).
Step 3: Test Your Webhook
Use the "Test Webhook" button in your dashboard to send a test payload:
{
"event": "payment.completed",
"payment": {
"id": "test_payment_1234567890",
"amount": 10.00,
"currency": "usd",
"status": "completed",
"customer_email": "test@example.com",
"metadata": {
"description": "Test payment for webhook verification"
},
"created_at": "2025-01-12T10:30:00.000Z",
"completed_at": "2025-01-12T10:30:15.000Z"
},
"shop": {
"id": "shop_abc123",
"shop_name": "My Shop"
},
"timestamp": "2025-01-12T10:30:15.000Z"
}
Webhook Events
Payzo sends webhooks for the following events:
| Event | Description |
|---|---|
payment.completed | Payment was successful and customer was charged |
payment.failed | Payment attempt failed (card declined, etc.) |
payment.refunded | Payment was refunded |
payment.expired | Payment link expired without completion |
Webhook Payload Structure
Every webhook contains:
{
"event": "payment.completed",
"payment": {
"id": "pay_abc123",
"amount": 50.00,
"currency": "usd",
"status": "completed",
"customer_email": "customer@example.com",
"customer_name": "John Doe",
"metadata": {
"order_id": "ORD-12345",
"user_id": "user_789"
},
"created_at": "2025-01-12T10:30:00.000Z",
"completed_at": "2025-01-12T10:30:15.000Z"
},
"shop": {
"id": "shop_abc123",
"shop_name": "My Awesome Store"
},
"timestamp": "2025-01-12T10:30:15.000Z"
}
Webhook Headers
Payzo sends these headers with every webhook:
Content-Type: application/json
User-Agent: Payzo-Webhook/1.0
X-Payzo-Event: payment.completed
X-Payzo-Signature: abc123def456...
Signature Header
The X-Payzo-Signature header contains an HMAC-SHA256 signature of the payload. Always verify this to ensure the webhook is authentic.
See Webhook Verification for implementation details.
Complete Example
Node.js / Express
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Webhook endpoint
app.post('/webhook/payzo', async (req, res) => {
const signature = req.headers['x-payzo-signature'];
const webhookSecret = process.env.PAYZO_WEBHOOK_SECRET;
// 1. Verify signature
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
if (signature !== expectedSignature) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// 2. Respond quickly
res.status(200).json({ received: true });
// 3. Process event
const { event, payment } = req.body;
try {
if (event === 'payment.completed') {
// Payment successful - complete the order
await completeOrder(payment.metadata.order_id, payment);
}
else if (event === 'payment.failed') {
// Payment failed - mark order as failed
await failOrder(payment.metadata.order_id);
}
else if (event === 'payment.refunded') {
// Payment refunded - reverse the order
await refundOrder(payment.metadata.order_id);
}
} catch (error) {
console.error('Webhook processing error:', error);
}
});
async function completeOrder(orderId, payment) {
// Update your database
await db.orders.update({
id: orderId,
status: 'completed',
payzo_payment_id: payment.id,
amount_paid: payment.amount,
completed_at: payment.completed_at
});
// Deliver digital goods, send confirmation email, etc.
await deliverProduct(orderId);
await sendConfirmationEmail(orderId);
}
Python / Flask
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
@app.route('/webhook/payzo', methods=['POST'])
def webhook():
signature = request.headers.get('X-Payzo-Signature')
webhook_secret = os.getenv('PAYZO_WEBHOOK_SECRET')
# 1. Verify signature
payload = json.dumps(request.json, separators=(',', ':'))
expected_signature = hmac.new(
webhook_secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
if signature != expected_signature:
print('Invalid webhook signature')
return jsonify({'error': 'Invalid signature'}), 401
# 2. Respond quickly
# 3. Process event
event = request.json.get('event')
payment = request.json.get('payment')
if event == 'payment.completed':
complete_order(payment['metadata']['order_id'], payment)
elif event == 'payment.failed':
fail_order(payment['metadata']['order_id'])
elif event == 'payment.refunded':
refund_order(payment['metadata']['order_id'])
return jsonify({'received': True}), 200
Retry Logic
If your webhook endpoint doesn't respond with a 2xx status code, Payzo will retry:
- Attempt 1: Immediate
- Attempt 2: After 2 seconds
- Attempt 3: After 4 seconds
Timeout: 10 seconds per attempt
Best Practices
1. Respond Quickly
Always return a 200 status code within 10 seconds:
// Good: Respond immediately
app.post('/webhook', async (req, res) => {
res.status(200).json({ received: true }); // Respond first
await processWebhook(req.body); // Process async
});
// Bad: Slow response
app.post('/webhook', async (req, res) => {
await longRunningProcess(); // Webhook will timeout!
res.status(200).json({ received: true });
});
2. Verify Signatures
Always verify the X-Payzo-Signature header. See Webhook Verification.
3. Handle Idempotency
Webhooks may be delivered multiple times. Use the payment.id to prevent duplicate processing:
async function completeOrder(payment) {
// Check if already processed
const existing = await db.payments.findOne({
payzo_payment_id: payment.id
});
if (existing && existing.status === 'completed') {
console.log('Payment already processed, skipping');
return;
}
// Process payment
await db.orders.update({...});
}
4. Use HTTPS
Your webhook endpoint must use HTTPS in production:
https://yoursite.com/webhook/payzo
http://yoursite.com/webhook/payzo
5. Log Webhook Events
Log all webhook events for debugging:
app.post('/webhook', async (req, res) => {
console.log('Webhook received:', {
event: req.body.event,
payment_id: req.body.payment.id,
timestamp: req.body.timestamp
});
// Save to database
await db.webhook_logs.create({
event: req.body.event,
payload: req.body,
received_at: new Date()
});
res.status(200).json({ received: true });
});
Troubleshooting
Webhook Not Receiving Events
- Check your webhook URL is correct in the dashboard
- Ensure your endpoint responds with 200 status code
- Verify your server is accessible from the internet (not localhost)
- Check firewall rules aren't blocking incoming requests
- Review webhook logs in your Payzo dashboard
Signature Verification Failing
- Ensure you're using the correct webhook secret
- Verify you're hashing the raw JSON payload (not pretty-printed)
- Check you're using HMAC-SHA256
- Make sure to stringify the payload exactly as received
Testing Webhooks Locally
Use a tool like ngrok to expose your local server:
# Start ngrok
ngrok http 3000
# Use the HTTPS URL in your dashboard
https://abc123.ngrok.io/webhook/payzo
Next Steps
- Webhook Verification - Secure your webhooks
- Webhook Events - Detailed event documentation
- Discord Notifications - Alternative notification method