Payzo Docs

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

  1. Go to your Dashboard
  2. Navigate to Shops
  3. Select your shop
  4. Click Edit or Settings
  5. Enter your webhook URL:
    https://yoursite.com/webhook/payzo
  6. 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:

EventDescription
payment.completedPayment was successful and customer was charged
payment.failedPayment attempt failed (card declined, etc.)
payment.refundedPayment was refunded
payment.expiredPayment 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

  1. Check your webhook URL is correct in the dashboard
  2. Ensure your endpoint responds with 200 status code
  3. Verify your server is accessible from the internet (not localhost)
  4. Check firewall rules aren't blocking incoming requests
  5. Review webhook logs in your Payzo dashboard

Signature Verification Failing

  1. Ensure you're using the correct webhook secret
  2. Verify you're hashing the raw JSON payload (not pretty-printed)
  3. Check you're using HMAC-SHA256
  4. 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