Payzo Docs

Webhook Events

Complete reference of all webhook events and their payloads

Event Types

Payzo sends webhooks for these events:

EventDescriptionWhen It's Sent
payment.completedPayment successfulCustomer successfully paid
payment.failedPayment failedCard declined or payment error
payment.refundedPayment refundedAdmin issued a refund
payment.expiredPayment expiredCheckout link expired unused

Event: payment.completed

Sent when a customer successfully completes a payment.

When to Handle

Handle this event to:

  • Mark orders as paid
  • Deliver digital goods
  • Send confirmation emails
  • Update inventory
  • Trigger fulfillment

Payload Example

{
  "event": "payment.completed",
  "payment": {
    "id": "pay_abc123def456",
    "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",
      "product_name": "Premium Package"
    },
    "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"
}

Example Handler

async function handlePaymentCompleted(payment) {
  const orderId = payment.metadata.order_id;
 
  // 1. Update order status
  await db.orders.update({
    id: orderId,
    status: 'paid',
    paid_at: payment.completed_at,
    payment_id: payment.id
  });
 
  // 2. Deliver product
  await deliverProduct(orderId);
 
  // 3. Send confirmation
  await sendEmail(payment.customer_email, {
    subject: 'Payment Confirmed',
    template: 'payment-success',
    data: {
      order_id: orderId,
      amount: payment.amount
    }
  });
 
  // 4. Update inventory
  await updateInventory(payment.metadata.product_id, -1);
 
  console.log(`Order ${orderId} completed successfully`);
}

Event: payment.failed

Sent when a payment attempt fails (card declined, insufficient funds, etc.).

When to Handle

Handle this event to:

  • Mark orders as failed
  • Send payment failure notification
  • Prompt customer to retry
  • Log failed attempts

Payload Example

{
  "event": "payment.failed",
  "payment": {
    "id": "pay_failed123",
    "amount": 25.00,
    "currency": "usd",
    "status": "failed",
    "customer_email": "customer@example.com",
    "customer_name": null,
    "metadata": {
      "order_id": "ORD-67890"
    },
    "created_at": "2025-01-12T11:00:00.000Z",
    "completed_at": "2025-01-12T11:00:05.000Z"
  },
  "shop": {
    "id": "shop_abc123",
    "shop_name": "My Awesome Store"
  },
  "timestamp": "2025-01-12T11:00:05.000Z"
}

Example Handler

async function handlePaymentFailed(payment) {
  const orderId = payment.metadata.order_id;
 
  // 1. Update order status
  await db.orders.update({
    id: orderId,
    status: 'payment_failed',
    failed_at: payment.completed_at
  });
 
  // 2. Send failure notification
  await sendEmail(payment.customer_email, {
    subject: 'Payment Failed',
    template: 'payment-failed',
    data: {
      order_id: orderId,
      reason: 'Your card was declined. Please try a different payment method.'
    }
  });
 
  // 3. Log the failure
  await db.payment_logs.create({
    order_id: orderId,
    event: 'payment_failed',
    payment_id: payment.id
  });
 
  console.log(`Payment failed for order ${orderId}`);
}

Event: payment.refunded

Sent when a refund is issued by Payzo support.

Refunds can be requested by:

  • Sellers: Contact us at /support or Discord
  • Customers: Submit a refund request at /support

When a refund is approved, the refund amount is automatically deducted from your seller balance (no additional fees). You'll receive this webhook to notify your system.

When to Handle

Handle this event to:

  • Reverse order fulfillment (e.g., revoke game items)
  • Update order status to refunded
  • Send refund confirmation to customer
  • Restore inventory
  • Update your accounting records

Payload Example

{
  "event": "payment.refunded",
  "payment": {
    "id": "pay_abc123def456",
    "amount": 100.00,
    "currency": "usd",
    "status": "refunded",
    "customer_email": "customer@example.com",
    "customer_name": "Jane Smith",
    "metadata": {
      "order_id": "ORD-11111",
      "refund_reason": "Customer request"
    },
    "created_at": "2025-01-10T09:00:00.000Z",
    "completed_at": "2025-01-10T09:05:00.000Z"
  },
  "shop": {
    "id": "shop_abc123",
    "shop_name": "My Awesome Store"
  },
  "timestamp": "2025-01-12T14:00:00.000Z"
}

Example Handler

async function handlePaymentRefunded(payment) {
  const orderId = payment.metadata.order_id;
 
  // 1. Update order status
  await db.orders.update({
    id: orderId,
    status: 'refunded',
    refunded_at: new Date()
  });
 
  // 2. Reverse fulfillment
  await revokeAccess(orderId);
 
  // 3. Restore inventory
  await updateInventory(payment.metadata.product_id, +1);
 
  // 4. Send refund notification
  await sendEmail(payment.customer_email, {
    subject: 'Refund Processed',
    template: 'refund-confirmation',
    data: {
      order_id: orderId,
      amount: payment.amount,
      refund_date: new Date()
    }
  });
 
  console.log(`Order ${orderId} refunded successfully`);
}

Event: payment.expired

Sent when a checkout link expires without being completed (rare).

When to Handle

Optional to handle:

  • Clean up pending orders
  • Log abandoned checkouts
  • Track conversion rates

Payload Example

{
  "event": "payment.expired",
  "payment": {
    "id": "pay_expired123",
    "amount": 15.00,
    "currency": "usd",
    "status": "expired",
    "customer_email": null,
    "customer_name": null,
    "metadata": {
      "order_id": "ORD-99999"
    },
    "created_at": "2025-01-11T10:00:00.000Z",
    "completed_at": null
  },
  "shop": {
    "id": "shop_abc123",
    "shop_name": "My Awesome Store"
  },
  "timestamp": "2025-01-12T10:00:00.000Z"
}

Example Handler

async function handlePaymentExpired(payment) {
  const orderId = payment.metadata.order_id;
 
  // 1. Mark as expired
  await db.orders.update({
    id: orderId,
    status: 'expired'
  });
 
  // 2. Log for analytics
  await db.analytics.log({
    event: 'checkout_abandoned',
    order_id: orderId,
    amount: payment.amount
  });
 
  console.log(`Payment expired for order ${orderId}`);
}

Complete Webhook Handler

Node.js Example

async function processWebhook(data) {
  const { event, payment } = data;
 
  // Route to appropriate handler
  switch (event) {
    case 'payment.completed':
      await handlePaymentCompleted(payment);
      break;
 
    case 'payment.failed':
      await handlePaymentFailed(payment);
      break;
 
    case 'payment.refunded':
      await handlePaymentRefunded(payment);
      break;
 
    case 'payment.expired':
      await handlePaymentExpired(payment);
      break;
 
    default:
      console.warn('Unknown webhook event:', event);
  }
}

Event Flow Diagram

Customer initiates payment

    [payment.pending]

    ┌────────────┐
    │ Card Check │
    └────┬───────┘

    ┌────┴────┐
    │         │
SUCCESS    DECLINED
    │         │
    ↓         ↓
payment.   payment.
completed  failed

    │  Refund requested?
    ├──────────→ payment.refunded

    │  Expires?
    └──────────→ payment.expired

Idempotency

Always check if you've already processed a webhook:

async function handlePaymentCompleted(payment) {
  // Check if already processed
  const existing = await db.payments.findOne({
    payzo_payment_id: payment.id,
    status: 'completed'
  });
 
  if (existing) {
    console.log('Payment already processed, skipping');
    return; // Don't process twice
  }
 
  // Process payment...
}

Testing Events

Using Dashboard

  1. Go to Dashboard > Shops
  2. Select your shop
  3. Click "Test Webhook"
  4. Sends payment.completed test event

Manual Testing

curl -X POST http://localhost:3000/webhook/payzo \
  -H "Content-Type: application/json" \
  -H "X-Payzo-Event: payment.completed" \
  -H "X-Payzo-Signature: your_signature_here" \
  -d '{
    "event": "payment.completed",
    "payment": {
      "id": "test_123",
      "amount": 10.00,
      "currency": "usd",
      "status": "completed",
      "metadata": {"order_id": "TEST-001"}
    }
  }'

Event Metadata

Use metadata to pass custom data through the payment flow:

// When creating payment
const payment = await createPayment({
  amount: 5000,
  metadata: {
    order_id: 'ORD-12345',
    user_id: 'user_789',
    product_id: 'prod_456',
    discount_code: 'SAVE20',
    affiliate_id: 'aff_123'
  }
});
 
// Metadata is returned in webhooks
function handlePaymentCompleted(payment) {
  const {
    order_id,
    user_id,
    product_id,
    discount_code,
    affiliate_id
  } = payment.metadata;
 
  // Use metadata to process order
}

Error Handling

async function processWebhook(data) {
  try {
    const { event, payment } = data;
 
    if (event === 'payment.completed') {
      await handlePaymentCompleted(payment);
    }
    // ... other events
 
  } catch (error) {
    // Log error but don't throw
    console.error('Webhook processing error:', error);
 
    // Save to error log
    await db.webhook_errors.create({
      event: data.event,
      payment_id: data.payment.id,
      error: error.message,
      stack: error.stack,
      timestamp: new Date()
    });
 
    // Alert team for critical errors
    if (isCriticalError(error)) {
      await alertTeam('Webhook processing failed', error);
    }
  }
}

Next Steps