Handle abandoned Paystack checkouts by tracking which transactions were initialized but never completed. Use the Paystack transaction list API with status=abandoned to find them. Send recovery emails with a fresh payment link. Reduce abandonment by minimizing steps before checkout, showing total cost upfront, and supporting multiple payment channels.
An abandoned checkout happens when you initialize a Paystack transaction but the customer never completes payment. The customer might have:
Paystack marks these transactions with a status of abandoned after a timeout period. You can see them in your Paystack dashboard under Transactions or query them via the API.
Importantly, "abandoned" is different from "failed." A failed transaction means the customer tried to pay but the payment did not go through (card declined, insufficient funds, bank error). An abandoned transaction means the customer never submitted a payment attempt at all.
Method 1: Query the Paystack API
Use the transaction list endpoint with a status filter to find abandoned transactions:
// Fetch abandoned transactions from the last 24 hours
async function getAbandonedCheckouts() {
var yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
var response = await fetch(
'https://api.paystack.co/transaction?status=abandoned&from=' + yesterday,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
var data = await response.json();
return data.data; // Array of abandoned transactions
}
Method 2: Track in your own database
When you initialize a transaction, record it in your database with a status of "pending." When the webhook fires or the customer is redirected back, update the status to "paid." Run a periodic job that finds records that have been "pending" for more than an hour. Those are your abandoned checkouts.
// When initializing
await db.paymentAttempts.create({
reference: reference,
order_id: orderId,
email: email,
amount: amount,
status: 'pending',
created_at: new Date(),
});
// Periodic job: find stale pending records
async function findAbandonedCheckouts() {
var oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
var abandoned = await db.paymentAttempts.findAll({
where: {
status: 'pending',
created_at: { lt: oneHourAgo },
},
});
return abandoned;
}
Method 2 is more reliable because it works even if Paystack's status update is delayed, and it lets you enrich the abandoned data with your own context (what product, what cart, which page the customer was on).
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
A well-timed recovery email can bring back 5-15% of abandoned checkouts. The key is speed, clarity, and a fresh payment link.
Timing
Content
Include what they were buying, the amount, and a prominent "Complete Payment" button. Do not include the old checkout link (it may be expired). Generate a fresh payment link.
// Generate a fresh payment link for recovery
async function createRecoveryLink(abandonedRecord) {
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: abandonedRecord.email,
amount: abandonedRecord.amount,
currency: 'NGN',
reference: 'recovery_' + abandonedRecord.order_id + '_' + Date.now(),
callback_url: 'https://yoursite.com/payment/callback',
metadata: {
order_id: abandonedRecord.order_id,
recovery_attempt: true,
},
}),
});
var data = await response.json();
return data.data.authorization_url;
}
Stop sending if paid
If the customer completes payment between your recovery email sends, stop the sequence immediately. Check the order status before sending each email. Sending "complete your purchase" to a customer who already paid is embarrassing and erodes trust.
The best abandoned checkout is one that never happens. Here are engineering and design decisions that reduce abandonment:
Show the total cost before checkout
If the customer sees 5,000 NGN on your product page but 5,750 NGN on the Paystack checkout (because of fees, VAT, or shipping), they bail. Show the final amount before they click "Pay." Absorb fees into the product price or display them clearly during the cart/order review step.
Minimize steps before payment
Every additional step (account creation, address form, phone verification) is a chance for the customer to leave. If you can, let customers pay as guests. Collect shipping details after payment when possible.
Support multiple payment channels
If a customer's card is declined and the only option is card, they leave. If bank transfer and USSD are also available, they can try another method. More channels means more completed payments.
Use inline checkout on desktop
The inline popup keeps the customer on your site. They see your branding in the background. The visual continuity reduces the feeling of being "sent somewhere else," which can trigger abandonment on redirect checkout.
Handle the "back" button
If the customer presses the browser back button during redirect checkout, they land on a stale page. Make sure your checkout page detects this and either reloads the current state or redirects to the cart.
Measure your abandonment rate and track it over time. The formula is simple:
Abandonment rate = (Initialized transactions - Completed transactions) / Initialized transactions * 100
// Calculate abandonment rate for the last 30 days
async function calculateAbandonmentRate() {
var thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
var total = await db.paymentAttempts.count({
where: { created_at: { gte: thirtyDaysAgo } },
});
var completed = await db.paymentAttempts.count({
where: {
created_at: { gte: thirtyDaysAgo },
status: 'paid',
},
});
var abandonmentRate = ((total - completed) / total) * 100;
return {
total: total,
completed: completed,
abandoned: total - completed,
rate: abandonmentRate.toFixed(1) + '%',
};
}
Break it down further:
Some "abandoned" checkouts are not truly abandoned. The customer might have completed the payment, but the webhook did not arrive (network issue, webhook URL down) or the redirect failed. Before sending a recovery email, verify the transaction status with Paystack:
// Before sending recovery email, check if actually abandoned
async function isActuallyAbandoned(reference) {
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
var data = await response.json();
if (data.data.status === 'success') {
// Payment was actually completed - do not send recovery email
// Instead, fulfill the order
return false;
}
return true; // Truly abandoned
}
This prevents the embarrassing scenario where a customer paid successfully but receives a "you forgot to pay" email because your webhook handler missed the event.
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