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
{
"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
| Field | Type | Description |
|---|---|---|
reference | string | ZevPay transaction reference |
merchant_reference | string | null | Your custom reference (if provided during initialization) |
amount | integer | Amount in kobo |
currency | string | Currency code (e.g., "NGN") |
status | string | Always "completed" for this event |
channel | string | Payment channel: "bank_transfer" or "payid" |
customer.email | string | Customer's email address |
customer.name | string | null | Customer's name |
metadata | object | null | Custom metadata from session initialization |
paid_at | string | ISO 8601 timestamp of payment confirmation |
fees | integer | Transaction fees in kobo (bank transfer only) |
payer | object | Payer information (varies by channel) |
Payer object by channel
Bank transfer
When channel is "bank_transfer":
{
"payer": {
"name": "John Doe",
"bank": "GTBank",
"account": "0123456789"
},
"fees": 7500
}| Field | Type | Description |
|---|---|---|
payer.name | string | Sender's name |
payer.bank | string | Sender's bank name |
payer.account | string | Sender's account number |
fees | integer | Transaction fees in kobo |
PayID
When channel is "payid":
{
"payer": {
"name": "John Doe"
}
}| Field | Type | Description |
|---|---|---|
payer.name | string | Sender'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:
| Field | Type | Description |
|---|---|---|
public_id | string | Unique public identifier for the invoice |
invoice_number | string | Invoice number (auto-generated or custom) |
status | string | Current status: draft, sent, partial, paid, overdue, cancelled |
customer_name | string | Customer name |
customer_email | string | Customer email |
subtotal | integer | Subtotal in kobo (before tax) |
tax_rate | number | Tax rate percentage (e.g., 7.5) |
tax_amount | integer | Tax amount in kobo |
total | integer | Total in kobo (subtotal + tax) |
amount_paid | integer | Cumulative amount paid so far in kobo |
currency | string | Currency code (always "NGN") |
due_date | string | Due date (ISO 8601) |
issued_at | string | null | Timestamp when invoice was sent (null for drafts) |
paid_at | string | null | Timestamp when invoice was fully paid |
merchant_reference | string | null | Your custom reference (if provided at creation) |
metadata | object | null | Your custom key-value metadata |
payment_url | string | URL 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
{
"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
{
"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:
| Field | Type | Description |
|---|---|---|
payment_amount | integer | This specific payment's amount in kobo |
amount_remaining | integer | Remaining balance after this payment in kobo |
payer_name | string | Name of the person who made the payment |
payment_channel | string | Payment channel: "bank_transfer" or "payid" |
Payload
{
"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_paidto equal or exceedtotal
Payload
{
"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_datehas passed and invoice is not fully paid (checked hourly by the system)
Payload
{
"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
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
reference | string | ZevPay transaction reference |
public_id | string | Public transaction identifier |
type | string | "bank_transfer" or "payid" |
status | string | Always "completed" for this event |
amount | integer | Transfer amount in kobo |
fees | integer | Transfer fees in kobo (0 for PayID) |
currency | string | Currency code ("NGN") |
narration | string | null | Transfer description |
recipient.name | string | Recipient name |
recipient.bank | string | null | Recipient bank (bank transfers only) |
recipient.account_number | string | null | Recipient account (bank transfers only) |
merchant_reference | string | null | Your custom reference |
metadata | object | null | Your custom metadata |
created_at | string | ISO 8601 creation timestamp |
completed_at | string | ISO 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
{
"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
{
"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
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:
# Using ngrok
ngrok http 3000
# Set webhook URL in dashboard to:
# https://your-id.ngrok.io/webhooks/zevpayWebhook 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