Paystack sends webhook events for charges (charge.success), transfers (transfer.success, transfer.failed, transfer.reversed), subscriptions (subscription.create, subscription.disable, subscription.not_renew, subscription.expiring_cards), invoices (invoice.create, invoice.payment_failed, invoice.update), refunds (refund.processed, refund.failed), disputes (dispute.create, dispute.remind), dedicated accounts (dedicatedaccount.assign.success, dedicatedaccount.assign.failed), customer identification (customeridentification.success, customeridentification.failed), and payment requests (paymentrequest.pending, paymentrequest.success).
Every Paystack webhook follows the same top-level structure:
{
"event": "event.type",
"data": {
// Event-specific fields
}
}
The event field is a string that tells you what happened. The data object contains the details. The structure of data varies by event type, but common fields include id, domain (test or live), status, reference, amount, and currency.
Your webhook handler should use the event field to route to the appropriate processing logic:
app.post(
'/webhooks/paystack',
express.raw({ type: 'application/json' }),
(req, res) => {
// Verify signature first (omitted for brevity)
const payload = JSON.parse(req.body.toString());
res.status(200).send('OK');
switch (payload.event) {
case 'charge.success':
handleChargeSuccess(payload.data);
break;
case 'transfer.success':
handleTransferSuccess(payload.data);
break;
case 'transfer.failed':
handleTransferFailed(payload.data);
break;
case 'transfer.reversed':
handleTransferReversed(payload.data);
break;
case 'subscription.create':
handleSubscriptionCreate(payload.data);
break;
// ... handle other events
default:
console.log('Unhandled event type: ' + payload.event);
}
}
);
Log unhandled event types instead of silently ignoring them. When Paystack adds new event types or you expand your integration, the logs tell you which events you are missing.
charge.success
Fires when a payment is completed successfully. This is the most important event for most applications. It tells you that money has moved from the customer to your Paystack balance.
Key payload fields:
data.reference - Your transaction referencedata.amount - Amount in the smallest currency unit (kobo for NGN, pesewas for GHS, cents for KES/ZAR)data.currency - Three-letter currency codedata.channel - Payment channel used (card, bank, ussd, mobile_money, bank_transfer, qr)data.status - Always "success" for this eventdata.customer - Object with email, customer_code, and other customer detailsdata.metadata - Whatever custom data you attached when initializing the transactiondata.paid_at - ISO 8601 timestamp of when the payment was completeddata.authorization - Card or payment method details (useful for recurring charges)What your handler should do: verify the amount matches what you expected, check that the reference corresponds to a pending order, mark the order as paid, and trigger fulfillment. See charge.success: the only event most apps actually need for the full guide.
There is no charge.failed webhook event. Paystack does not send a webhook when a charge fails. Failed charges are communicated through the checkout flow (the customer sees an error in the popup or redirect) and can be checked via the Verify Transaction API.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Transfer events fire when you send money from your Paystack balance to a bank account (payouts, vendor payments, withdrawals). If your application only collects payments and never sends money out, you do not need these.
transfer.success
The transfer was completed successfully. The money has left your Paystack balance and reached the recipient's bank account.
transfer.failed
The transfer failed. The money remains in your Paystack balance. Common reasons: invalid account number, bank rejection, insufficient balance. The data.reason or data.complete_message field may contain details about why it failed.
transfer.reversed
A transfer that initially succeeded was reversed by the bank. This is rare but important. The money comes back to your Paystack balance. You must update your records to reflect that the recipient did not actually receive the funds.
Key payload fields (all three events):
data.reference - Your transfer referencedata.transfer_code - Paystack's transfer identifierdata.amount - Amount in smallest currency unitdata.recipient - Object with recipient details (name, account number, bank)data.status - "success", "failed", or "reversed"data.reason - Reason for failure or reversal (when applicable)See handling transfer events for the full implementation guide.
These events track the lifecycle of recurring billing subscriptions created through Paystack.
subscription.create
A new subscription was created. This fires when a customer subscribes to a plan, either through the hosted checkout or via the API.
subscription.disable
A subscription was cancelled (disabled). The customer or you (via the API or Dashboard) deactivated the subscription. No further charges will be attempted.
subscription.not_renew
A subscription will not renew at the next billing cycle. This fires when a customer cancels before the next charge date. The subscription remains active until the current period ends, but no new invoice will be generated.
subscription.expiring_cards
A notification that cards attached to subscriptions are about to expire. This gives you a chance to notify customers and ask them to update their payment method before the next charge fails.
Key payload fields:
data.subscription_code - Paystack's subscription identifierdata.plan - Object with plan details (name, plan_code, amount, interval)data.customer - Object with customer detailsdata.status - "active", "non-renewing", or "cancelled"data.next_payment_date - When the next charge will happen (if active)See handling subscription and invoice events for the full guide.
Invoices are generated automatically for subscription renewals. These events track their lifecycle.
invoice.create
An invoice was generated for an upcoming subscription charge. This happens before the charge is attempted. You can use this to notify the customer that they will be charged soon.
invoice.payment_failed
The automatic charge for an invoice failed. The customer's card was declined, expired, or had insufficient funds. Paystack may retry the charge depending on the plan configuration.
invoice.update
An invoice was updated. This typically happens after a successful charge, when the invoice status changes from "pending" to "paid".
Key payload fields:
data.invoice_code - Paystack's invoice identifierdata.subscription - The subscription this invoice belongs todata.amount - Invoice amountdata.status - "pending", "paid", or "failed"data.paid_at - When the invoice was paid (if applicable)data.transaction - The transaction associated with the payment (if paid)Note: A successful subscription renewal generates both an invoice.update (invoice marked as paid) and a charge.success (the actual payment). You can handle either one, but be careful not to grant double value if you handle both.
Refund events fire when you process a refund through the Paystack API or Dashboard.
refund.processed
A refund was completed successfully. The money has been returned to the customer.
refund.failed
A refund attempt failed. This can happen if the customer's card issuer rejects the refund or there is a processing error.
Key payload fields:
data.transaction - The original transaction that is being refundeddata.amount - Refund amount (may be less than the original for partial refunds)data.status - "processed" or "failed"data.customer_note - Note visible to the customerdata.merchant_note - Note visible only to youWhat your handler should do: update the order status to "refunded" (or "partially refunded"), adjust your accounting records, and optionally notify the customer that their refund has been processed or failed.
See handling refund events for more detail. For the API side of refunds, see Paystack refunds: full, partial, and programmatic.
Disputes (chargebacks) happen when a customer contacts their bank to reverse a charge. These are time-sensitive and require your attention.
dispute.create
A new dispute was opened against one of your transactions. You typically have a limited window (often 24-72 hours, depending on the card network and bank) to respond with evidence.
dispute.remind
A reminder that you have an unresolved dispute approaching its resolution deadline. This is Paystack nudging you to respond before the deadline passes.
Key payload fields:
data.transaction - The transaction being disputeddata.status - Current dispute statusdata.due_at - Deadline for your responsedata.message - Details about the dispute reasondata.resolution - How the dispute was resolved (if resolved)What your handler should do: immediately alert your team (Slack, email, PagerDuty). Disputes have strict deadlines. If you do not respond in time, you lose automatically. Pull together evidence (proof of delivery, customer communication, usage logs) and submit it through the Paystack Dashboard or API.
See handling dispute events and disputes and chargebacks: the developer playbook.
Dedicated (virtual) bank accounts are bank account numbers that Paystack assigns to your customers. When someone transfers money to the virtual account, Paystack credits your balance and sends a webhook.
dedicatedaccount.assign.success
A dedicated virtual account was successfully assigned to a customer. This fires after you request account creation through the API, and the bank confirms the account is ready. The payload includes the account number, bank name, and the customer it was assigned to.
dedicatedaccount.assign.failed
The request to create a dedicated account failed. This can happen due to compliance checks, bank-side issues, or missing customer information (like BVN for Nigerian accounts).
Key payload fields:
data.dedicated_account - Object with the virtual account details (account number, bank name, bank code)data.customer - The customer this account belongs toNote: When a customer sends money to a dedicated virtual account, you receive a charge.success event for the actual payment, not a dedicated account-specific event. The dedicatedaccount.assign events are only about the account creation lifecycle.
See handling dedicatedaccount.assign events and dedicated virtual accounts: complete implementation.
These events report the result of KYC (Know Your Customer) identity verification checks. Paystack offers identity verification for Nigerian customers using BVN (Bank Verification Number).
customeridentification.success
The customer's identity was verified successfully. Their information matches what is on record with the bank.
customeridentification.failed
The identity verification failed. The information provided did not match, or there was a processing error.
Key payload fields:
data.customer_id - The customer who was verifieddata.customer_code - Paystack's customer identifierdata.identification - Object with the identification type and resultWhat your handler should do: update the customer's verification status in your database. If verification is required for certain features (like dedicated virtual accounts or higher transaction limits), enable those features upon success. On failure, prompt the customer to retry or provide alternative identification.
Start with the minimum and add events as your product requires them:
Every app that accepts payments: charge.success
Apps that send money out (payouts, vendor payments): Add transfer.success, transfer.failed, transfer.reversed
Apps with recurring billing: Add subscription.create, subscription.disable, invoice.create, invoice.payment_failed
Apps that process refunds: Add refund.processed, refund.failed
Apps using dedicated virtual accounts: Add dedicatedaccount.assign.success, dedicatedaccount.assign.failed
Apps that need KYC verification: Add customeridentification.success, customeridentification.failed
All apps in production: Add dispute.create, dispute.remind (even if disputes are rare, you need to respond quickly when they happen)
You do not need to handle every event from day one. Paystack sends all events to your single webhook URL regardless of whether you handle them. Your handler can log and ignore events it does not care about. When you need a new event type, add a case to your switch statement.
This article is part of the Paystack webhooks engineering guide.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course