Every Paystack Webhook Event Explained
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).
Webhook Payload Structure
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 Events
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.
Transfer Events
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.
Subscription Events
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.
Invoice Events
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
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 you
What 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.
Dispute Events
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 Account Events
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 to
Note: 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.
Customer Identification Events
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 result
What 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.
Which Events You Actually Need
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.
Key Takeaways
- ✓Most applications only need charge.success. Add other events as your product requirements grow.
- ✓Transfer events (success, failed, reversed) are critical if your application sends money out to bank accounts.
- ✓Subscription and invoice events let you track recurring billing lifecycle without polling the API.
- ✓Refund and dispute events help you keep your records in sync and respond to chargebacks within the required window.
- ✓Dedicated account events fire when virtual bank accounts are assigned to customers for receiving payments.
- ✓Customer identification events report the result of KYC/identity verification checks.
- ✓The event field in every webhook payload tells you the event type. Route your handler based on this field.
Frequently Asked Questions
- Does Paystack send a webhook when a payment fails?
- No. There is no charge.failed webhook event. Paystack only sends charge.success when a payment completes. Failed payment attempts are communicated through the checkout UI (the customer sees an error) and can be checked via the Verify Transaction API. If you need to track failed attempts, use the Paystack Dashboard or the Transaction API.
- Can I choose which webhook events Paystack sends?
- No. Paystack sends all events to your webhook URL. You cannot filter or subscribe to specific event types in the Dashboard. Your handler receives everything and decides which events to process. Use a switch statement or event router in your code to handle the events you care about and log the rest.
- What is the difference between invoice.update and charge.success for subscription renewals?
- A successful subscription renewal triggers both events. charge.success represents the actual payment transaction. invoice.update represents the invoice being marked as paid. You can handle either one to grant value, but do not handle both in a way that grants value twice. Most developers use charge.success because it follows the same pattern as one-time payments.
- How do I know if Paystack has added new webhook event types?
- Check the Paystack API documentation periodically and follow their developer changelog. New features (like new payment channels or financial products) may introduce new event types. Your handler should log any event type it does not recognize so you know when new events start arriving.
- Do test mode and live mode send the same webhook events?
- Yes. The event types and payload structure are the same. The difference is the data.domain field: it is "test" for test mode events and "live" for live mode events. Both modes use the same webhook URL. If you need to distinguish between test and live events in your handler, check the domain field.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon