Skip to content

Events Reference

This page documents all webhook event types, their payloads, and when they are triggered.

charge.success

Sent when a checkout payment is completed successfully. This is the primary event you should handle to fulfill orders.

When it fires

  • Customer completes a bank transfer to the virtual account
  • Customer sends payment via ZevPay ID (PayID)

Payload

json
{
  "event": "charge.success",
  "data": {
    "reference": "ZVP-CKO-S-abc123",
    "merchant_reference": "order_12345",
    "amount": 500000,
    "currency": "NGN",
    "status": "completed",
    "channel": "bank_transfer",
    "customer": {
      "email": "customer@example.com",
      "name": "John Doe"
    },
    "metadata": {
      "orderId": "12345"
    },
    "paid_at": "2026-03-07T19:42:00.000Z",
    "fees": 7500,
    "payer": {
      "name": "John Doe",
      "bank": "GTBank",
      "account": "0123456789"
    }
  }
}

Fields

FieldTypeDescription
referencestringZevPay transaction reference
merchant_referencestring | nullYour custom reference (if provided during initialization)
amountintegerAmount in kobo
currencystringCurrency code (e.g., "NGN")
statusstringAlways "completed" for this event
channelstringPayment channel: "bank_transfer" or "payid"
customer.emailstringCustomer's email address
customer.namestring | nullCustomer's name
metadataobject | nullCustom metadata from session initialization
paid_atstringISO 8601 timestamp of payment confirmation
feesintegerTransaction fees in kobo (bank transfer only)
payerobjectPayer information (varies by channel)

Payer object by channel

Bank transfer

When channel is "bank_transfer":

json
{
  "payer": {
    "name": "John Doe",
    "bank": "GTBank",
    "account": "0123456789"
  },
  "fees": 7500
}
FieldTypeDescription
payer.namestringSender's name
payer.bankstringSender's bank name
payer.accountstringSender's account number
feesintegerTransaction fees in kobo

PayID

When channel is "payid":

json
{
  "payer": {
    "name": "John Doe"
  }
}
FieldTypeDescription
payer.namestringSender's ZevPay name

INFO

PayID payments do not include fees, payer.bank, or payer.account fields.

Invoice events — base fields

All invoice webhook events share the same base payload structure. These fields are always present:

FieldTypeDescription
public_idstringUnique public identifier for the invoice
invoice_numberstringInvoice number (auto-generated or custom)
statusstringCurrent status: draft, sent, partial, paid, overdue, cancelled
customer_namestringCustomer name
customer_emailstringCustomer email
subtotalintegerSubtotal in kobo (before tax)
tax_ratenumberTax rate percentage (e.g., 7.5)
tax_amountintegerTax amount in kobo
totalintegerTotal in kobo (subtotal + tax)
amount_paidintegerCumulative amount paid so far in kobo
currencystringCurrency code (always "NGN")
due_datestringDue date (ISO 8601)
issued_atstring | nullTimestamp when invoice was sent (null for drafts)
paid_atstring | nullTimestamp when invoice was fully paid
merchant_referencestring | nullYour custom reference (if provided at creation)
metadataobject | nullYour custom key-value metadata
payment_urlstringURL for customer to pay the invoice

invoice.created

Sent when a new invoice is created via the API.

When it fires

  • A new invoice is created via POST /v1/checkout/invoice

Payload

json
{
  "event": "invoice.created",
  "data": {
    "public_id": "abc123def456ghi78",
    "invoice_number": "INV-202603-001",
    "status": "draft",
    "customer_name": "Chidi Okonkwo",
    "customer_email": "chidi@example.com",
    "subtotal": 41000000,
    "tax_rate": 7.5,
    "tax_amount": 3075000,
    "total": 44075000,
    "amount_paid": 0,
    "currency": "NGN",
    "due_date": "2026-03-31T00:00:00.000Z",
    "issued_at": null,
    "paid_at": null,
    "merchant_reference": "order_12345",
    "metadata": { "orderId": "12345" },
    "payment_url": "https://secure.zevpaycheckout.com/inv/abc123def456ghi78"
  }
}

invoice.sent

Sent when an invoice is transitioned from draft to sent, making it payable.

When it fires

  • Invoice is sent via POST /v1/checkout/invoice/:publicId/send

Payload

json
{
  "event": "invoice.sent",
  "data": {
    "public_id": "abc123def456ghi78",
    "invoice_number": "INV-202603-001",
    "status": "sent",
    "customer_name": "Chidi Okonkwo",
    "customer_email": "chidi@example.com",
    "subtotal": 41000000,
    "tax_rate": 7.5,
    "tax_amount": 3075000,
    "total": 44075000,
    "amount_paid": 0,
    "currency": "NGN",
    "due_date": "2026-03-31T00:00:00.000Z",
    "issued_at": "2026-03-08T10:30:00.000Z",
    "paid_at": null,
    "merchant_reference": "order_12345",
    "metadata": { "orderId": "12345" },
    "payment_url": "https://secure.zevpaycheckout.com/inv/abc123def456ghi78"
  }
}

TIP

The issued_at timestamp is set when the invoice is sent. Use this to track when an invoice became active.

invoice.payment_received

Sent when a payment is received on an invoice (partial or full). This is the most important event for tracking payment progress — it fires for every payment, including the final one that fully pays the invoice.

When it fires

  • Customer completes a bank transfer for an invoice payment
  • Customer sends payment via PayID for an invoice payment

Additional fields

In addition to the base fields, this event includes:

FieldTypeDescription
payment_amountintegerThis specific payment's amount in kobo
amount_remainingintegerRemaining balance after this payment in kobo
payer_namestringName of the person who made the payment
payment_channelstringPayment channel: "bank_transfer" or "payid"

Payload

json
{
  "event": "invoice.payment_received",
  "data": {
    "public_id": "abc123def456ghi78",
    "invoice_number": "INV-202603-001",
    "status": "partial",
    "customer_name": "Chidi Okonkwo",
    "customer_email": "chidi@example.com",
    "subtotal": 41000000,
    "tax_rate": 7.5,
    "tax_amount": 3075000,
    "total": 44075000,
    "amount_paid": 20000000,
    "currency": "NGN",
    "due_date": "2026-03-31T00:00:00.000Z",
    "issued_at": "2026-03-08T10:30:00.000Z",
    "paid_at": null,
    "merchant_reference": "order_12345",
    "metadata": { "orderId": "12345" },
    "payment_url": "https://secure.zevpaycheckout.com/inv/abc123def456ghi78",
    "payment_amount": 20000000,
    "amount_remaining": 24075000,
    "payer_name": "CHIDI OKONKWO",
    "payment_channel": "bank_transfer"
  }
}

INFO

When the payment fully pays the invoice, status will be "paid", amount_remaining will be 0, and paid_at will be set. You will also receive a separate invoice.paid event immediately after.

invoice.paid

Sent when an invoice is fully paid (cumulative payments reach or exceed the total). This event always follows an invoice.payment_received event for the final payment.

When it fires

  • A payment completes that brings amount_paid to equal or exceed total

Payload

json
{
  "event": "invoice.paid",
  "data": {
    "public_id": "abc123def456ghi78",
    "invoice_number": "INV-202603-001",
    "status": "paid",
    "customer_name": "Chidi Okonkwo",
    "customer_email": "chidi@example.com",
    "subtotal": 41000000,
    "tax_rate": 7.5,
    "tax_amount": 3075000,
    "total": 44075000,
    "amount_paid": 44075000,
    "currency": "NGN",
    "due_date": "2026-03-31T00:00:00.000Z",
    "issued_at": "2026-03-08T10:30:00.000Z",
    "paid_at": "2026-03-10T14:22:00.000Z",
    "merchant_reference": "order_12345",
    "metadata": { "orderId": "12345" },
    "payment_url": "https://secure.zevpaycheckout.com/inv/abc123def456ghi78"
  }
}

TIP

Use invoice.paid to trigger order fulfillment. The paid_at timestamp tells you exactly when the invoice was fully settled. The payment_url now resolves to a receipt page.

invoice.overdue

Sent when an invoice is automatically marked as overdue.

When it fires

  • Invoice due_date has passed and invoice is not fully paid (checked hourly by the system)

Payload

json
{
  "event": "invoice.overdue",
  "data": {
    "public_id": "abc123def456ghi78",
    "invoice_number": "INV-202603-001",
    "status": "overdue",
    "customer_name": "Chidi Okonkwo",
    "customer_email": "chidi@example.com",
    "subtotal": 41000000,
    "tax_rate": 7.5,
    "tax_amount": 3075000,
    "total": 44075000,
    "amount_paid": 20000000,
    "currency": "NGN",
    "due_date": "2026-03-31T00:00:00.000Z",
    "issued_at": "2026-03-08T10:30:00.000Z",
    "paid_at": null,
    "merchant_reference": "order_12345",
    "metadata": { "orderId": "12345" },
    "payment_url": "https://secure.zevpaycheckout.com/inv/abc123def456ghi78"
  }
}

INFO

Overdue invoices can still receive payments. If the customer pays the full remaining balance, the invoice transitions to paid and you'll receive invoice.payment_received and invoice.paid events. Use this event to send payment reminders to your customers.

invoice.cancelled

Sent when an invoice is cancelled by the merchant.

When it fires

  • Invoice is cancelled via POST /v1/checkout/invoice/:publicId/cancel

Payload

json
{
  "event": "invoice.cancelled",
  "data": {
    "public_id": "abc123def456ghi78",
    "invoice_number": "INV-202603-001",
    "status": "cancelled",
    "customer_name": "Chidi Okonkwo",
    "customer_email": "chidi@example.com",
    "subtotal": 41000000,
    "tax_rate": 7.5,
    "tax_amount": 3075000,
    "total": 44075000,
    "amount_paid": 0,
    "currency": "NGN",
    "due_date": "2026-03-31T00:00:00.000Z",
    "issued_at": "2026-03-08T10:30:00.000Z",
    "paid_at": null,
    "merchant_reference": "order_12345",
    "metadata": { "orderId": "12345" },
    "payment_url": "https://secure.zevpaycheckout.com/inv/abc123def456ghi78"
  }
}

transfer.success

Sent when a transfer is completed successfully — funds have been delivered to the recipient.

When it fires

  • Bank transfer is confirmed by the banking provider
  • PayID transfer completes instantly

Payload

json
{
  "event": "transfer.success",
  "data": {
    "reference": "ZVP-TRF-abc123def456",
    "public_id": "xK9mQ2pL4vR7nW3jY",
    "type": "bank_transfer",
    "status": "completed",
    "amount": 500000,
    "fees": 2625,
    "currency": "NGN",
    "narration": "Vendor payment",
    "recipient": {
      "name": "JOHN DOE",
      "bank": "Guaranty Trust Bank",
      "account_number": "0123456789"
    },
    "merchant_reference": "payout_001",
    "metadata": {
      "invoice_id": "INV-2026-001"
    },
    "created_at": "2026-03-08T14:30:00.000Z",
    "completed_at": "2026-03-08T14:30:02.000Z"
  }
}

Fields

FieldTypeDescription
referencestringZevPay transaction reference
public_idstringPublic transaction identifier
typestring"bank_transfer" or "payid"
statusstringAlways "completed" for this event
amountintegerTransfer amount in kobo
feesintegerTransfer fees in kobo (0 for PayID)
currencystringCurrency code ("NGN")
narrationstring | nullTransfer description
recipient.namestringRecipient name
recipient.bankstring | nullRecipient bank (bank transfers only)
recipient.account_numberstring | nullRecipient account (bank transfers only)
merchant_referencestring | nullYour custom reference
metadataobject | nullYour custom metadata
created_atstringISO 8601 creation timestamp
completed_atstringISO 8601 completion timestamp

transfer.failed

Sent when a bank transfer fails. The debited amount is automatically reversed to the source wallet.

When it fires

  • Bank rejects the transfer (invalid account, bank downtime, etc.)

Payload

json
{
  "event": "transfer.failed",
  "data": {
    "reference": "ZVP-TRF-xyz789ghi012",
    "public_id": "aB3cD4eF5gH6iJ7kL",
    "type": "bank_transfer",
    "status": "failed",
    "amount": 500000,
    "fees": 2625,
    "currency": "NGN",
    "narration": "Vendor payment",
    "recipient": {
      "name": "JOHN DOE",
      "bank": "Guaranty Trust Bank",
      "account_number": "0123456789"
    },
    "merchant_reference": "payout_002",
    "metadata": null,
    "created_at": "2026-03-08T15:00:00.000Z",
    "completed_at": null
  }
}

INFO

When a transfer fails, the full amount (including fees) is reversed to your wallet. You do not need to handle the reversal manually. PayID transfers cannot fail — they either succeed instantly or are rejected before debiting.

transfer.reversed

Sent when a previously completed transfer is reversed (e.g., by admin action or bank recall).

When it fires

  • Admin initiates a transfer reversal
  • Bank recalls a completed transfer

Payload

json
{
  "event": "transfer.reversed",
  "data": {
    "reference": "ZVP-TRF-abc123def456",
    "public_id": "xK9mQ2pL4vR7nW3jY",
    "type": "bank_transfer",
    "status": "reversed",
    "amount": 500000,
    "fees": 2625,
    "currency": "NGN",
    "narration": "Vendor payment",
    "recipient": {
      "name": "JOHN DOE",
      "bank": "Guaranty Trust Bank",
      "account_number": "0123456789"
    },
    "merchant_reference": "payout_001",
    "metadata": null,
    "created_at": "2026-03-08T14:30:00.000Z",
    "completed_at": null
  }
}

Handling events

js
app.post('/webhooks/zevpay', (req, res) => {
  // Verify signature first (see Verifying Signatures)

  const { event, data } = req.body;

  switch (event) {
    case 'charge.success':
      handleSuccessfulPayment(data);
      break;
    case 'invoice.paid':
      handleInvoicePaid(data);
      break;
    case 'invoice.payment_received':
      handleInvoicePayment(data);
      break;
    case 'transfer.success':
      handleTransferSuccess(data);
      break;
    case 'transfer.failed':
      handleTransferFailed(data);
      break;
    default:
      console.log('Unhandled event:', event);
  }

  res.status(200).send('OK');
});

async function handleSuccessfulPayment(data) {
  // Check if already processed (idempotency)
  const existing = await db.findOrder({ reference: data.reference });
  if (existing?.fulfilled) return;

  // Fulfill the order
  await db.updateOrder({
    reference: data.reference,
    status: 'paid',
    amount: data.amount,
    paidAt: data.paid_at,
    channel: data.channel,
    payerName: data.payer.name,
    fulfilled: true,
  });

  // Send confirmation email
  await sendConfirmationEmail(data.customer.email, data);
}

Testing webhooks

Test mode

Use your test mode API keys to trigger test webhooks. Test transactions behave the same as live transactions but use test credentials.

Local development

To receive webhooks locally during development, use a tunneling tool:

bash
# Using ngrok
ngrok http 3000

# Set webhook URL in dashboard to:
# https://your-id.ngrok.io/webhooks/zevpay

Webhook logs

View delivery logs in the ZevPay Dashboard under Transactions → Webhook Logs. Each log entry shows:

  • Event type
  • Delivery status (delivered / failed)
  • HTTP status code
  • Response time
  • Request and response bodies

ZevPay Checkout Developer Documentation