Skip to main content

Overview

Webhooks allow you to receive real-time notifications for payment events such as completion, failure, and refunds. ZAFA PAY sends HTTP POST requests to your registered URL when payment status changes.
The Webhook Secret can be found in the “Merchant Settings” section of the merchant dashboard (https://app.zafapay.com).

Event Types

EventDescription
payment.succeededPayment completed successfully
payment.failedPayment failed
payment.canceledPayment canceled
payment.refundedRefund completed
payment.chargebackChargeback occurred

Payload

event
string
Event type (e.g., payment.succeeded)
transaction_id
string
Transaction ID
merchant_id
string
Merchant ID
merchant_name
string
Merchant name
status
string
Payment status (succeeded, failed, canceled, refunded, chargeback)
amount
string
Payment amount (string format, e.g., "100.00")
currency
string
Currency code
payment_method
string
Payment method (card, depot, etc.)
payment_method_id
string
Saved card ID (pmi_xxx format). Only included for recurring payments or when save_card was used
is_recurring
boolean
true for recurring (subscription) payments
save_card
boolean
Only included as true when the card was saved for future use
external_id
string
Merchant’s order ID (the value specified when creating the payment)
product_name
string
Product name (the value specified when creating the payment)
customer_id
string
Customer ID (the value specified when creating the payment)
email
string
Customer’s email address (the value specified when creating the payment)
tel
string
Customer’s phone number (the value specified when creating the payment)
amount_refunded
string
Refunded amount (string format. payment.refunded event only)
error
string
Error message (payment.failed event only)
card_brand
string
Card brand (visa, mastercard, amex, jcb, etc.)
card_last4
string
Last 4 digits of the card number
cardholder_name
string
Cardholder name
card_exp_month
number
Card expiration month
card_exp_year
number
Card expiration year
metadata
object
Metadata specified when creating the payment
created_at
string
Transaction creation timestamp (ISO 8601 format)
timestamp
string
Webhook sent timestamp (ISO 8601 format)

Payload Examples

payment.succeeded (Payment Successful)

{
  "event": "payment.succeeded",
  "transaction_id": "tx_abc123",
  "merchant_id": "acct_12345",
  "merchant_name": "Sample Store",
  "status": "succeeded",
  "amount": "100.00",
  "currency": "usd",
  "payment_method": "card",
  "payment_method_id": "pmi_abc123",
  "is_recurring": false,
  "save_card": true,
  "external_id": "order_12345",
  "product_name": "Premium Plan",
  "customer_id": "cust_12345",
  "email": "customer@example.com",
  "tel": "09012345678",
  "card_brand": "visa",
  "card_last4": "4242",
  "cardholder_name": "TARO YAMADA",
  "card_exp_month": 12,
  "card_exp_year": 2028,
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-01-15T10:31:00Z"
}
  • payment_method_id is only included when save_card was used or for recurring payments
  • save_card is only included as true when the card was saved in the initial payment
  • is_recurring is true for recurring payments

payment.failed (Payment Failed)

{
  "event": "payment.failed",
  "transaction_id": "tx_abc123",
  "merchant_id": "acct_12345",
  "merchant_name": "Sample Store",
  "status": "failed",
  "amount": "100.00",
  "currency": "usd",
  "payment_method": "card",
  "is_recurring": false,
  "external_id": "order_12345",
  "product_name": "Premium Plan",
  "customer_id": "cust_12345",
  "email": "customer@example.com",
  "tel": "09012345678",
  "error": "Your card was declined.",
  "card_brand": "visa",
  "card_last4": "4242",
  "cardholder_name": "TARO YAMADA",
  "card_exp_month": 12,
  "card_exp_year": 2028,
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-01-15T10:31:00Z"
}
The error field is only included in payment.failed events.

payment.refunded (Refund Completed)

{
  "event": "payment.refunded",
  "transaction_id": "tx_abc123",
  "merchant_id": "acct_12345",
  "merchant_name": "Sample Store",
  "status": "refunded",
  "amount": "100.00",
  "currency": "usd",
  "payment_method": "card",
  "is_recurring": false,
  "external_id": "order_12345",
  "product_name": "Premium Plan",
  "customer_id": "cust_12345",
  "email": "customer@example.com",
  "tel": "09012345678",
  "amount_refunded": "50.00",
  "card_brand": "visa",
  "card_last4": "4242",
  "cardholder_name": "TARO YAMADA",
  "card_exp_month": 12,
  "card_exp_year": 2028,
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-01-20T14:00:00Z"
}
The amount_refunded field is only included in payment.refunded events. For partial refunds, it shows the refunded amount.

payment.chargeback (Chargeback Occurred)

{
  "event": "payment.chargeback",
  "transaction_id": "tx_abc123",
  "merchant_id": "acct_12345",
  "merchant_name": "Sample Store",
  "status": "chargeback",
  "amount": "100.00",
  "currency": "usd",
  "payment_method": "card",
  "is_recurring": false,
  "external_id": "order_12345",
  "product_name": "Premium Plan",
  "customer_id": "cust_12345",
  "email": "customer@example.com",
  "tel": "09012345678",
  "card_brand": "visa",
  "card_last4": "4242",
  "cardholder_name": "TARO YAMADA",
  "card_exp_month": 12,
  "card_exp_year": 2028,
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-02-01T09:00:00Z"
}
When a chargeback occurs, the transaction amount may be debited from the merchant. Please respond promptly.

Signature Verification

Webhook requests include a signature header. Verify this signature to confirm the request was sent by ZAFA PAY.

Signature Header

EnvironmentHeader Name
SandboxX-Zafapay-Signature-Sandbox
ProductionX-Zafapay-Signature

Verification Method

The signature is an HMAC-SHA256 hash of the request body (JSON string).
Node.js
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return signature === expectedSignature;
}

// Express.js example
app.post('/webhooks/zafapay', express.json(), (req, res) => {
  const signature = req.headers['x-zafapay-signature-sandbox'];
  const secret = process.env.ZAFAPAY_WEBHOOK_SECRET;

  if (!verifyWebhookSignature(req.body, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process event
  const { event, transaction_id, status } = req.body;

  switch (event) {
    case 'payment.succeeded':
      // Handle successful payment
      break;
    case 'payment.failed':
      // Handle failed payment
      break;
    case 'payment.refunded':
      // Handle refund
      break;
  }

  res.json({ received: true });
});

Response

Return HTTP status code 2xx when the webhook is successfully received.
{
  "received": true
}

Retry

ZAFA PAY automatically retries webhooks in the following cases:
  • HTTP status code other than 2xx is returned
  • Connection timeout occurs (10 seconds)

Retry Schedule

AttemptDelay After Failure
1stImmediate
2nd1 minute later
3rd5 minutes later
4th30 minutes later
5th2 hours later
6th6 hours later
A total of 6 retries are executed over approximately 9 hours. If all retries fail, the event is moved to a Dead Letter Queue (DLQ) for manual intervention.
To prevent duplicate notifications from retries, ensure idempotency using transaction_id as the key.

Best Practices

1

Always Verify Signature

Signature verification is essential to prevent unauthorized requests
2

Ensure Idempotency

The same event may be sent multiple times. Use transaction_id as a key to prevent duplicate processing
3

Return Response Quickly

Process webhooks asynchronously and return 200 immediately. Long processing times will cause timeout retries
4

Log Errors

Log received payloads and processing results for debugging