Bonaventure OgetoBy Bonaventure Ogeto|

Handling Abandoned Paystack Checkouts

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.

What Counts as an Abandoned Checkout

An abandoned checkout happens when you initialize a Paystack transaction but the customer never completes payment. The customer might have:

  • Closed the browser tab or the Paystack popup before paying
  • Navigated away from the checkout page
  • Started entering card details but stopped
  • Seen the price on the checkout page and decided not to pay
  • Lost their internet connection during checkout
  • Been distracted by something else

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.

Detecting Abandoned Checkouts

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).

Recovery Emails

A well-timed recovery email can bring back 5-15% of abandoned checkouts. The key is speed, clarity, and a fresh payment link.

Timing

  • First email: 1 hour after abandonment. The customer is still warm. Subject: "You left something behind" or "Complete your purchase."
  • Second email: 24 hours later. A gentle reminder. Subject: "Still interested?" or "Your order is waiting."
  • Third email: 3 days later. Final reminder with urgency. Subject: "Last chance to complete your order."

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.

Reducing Abandonment Before It Happens

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.

Tracking Abandonment Metrics

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:

  • By payment channel: Do USSD checkouts have higher abandonment than card? Maybe the USSD flow is confusing for your audience.
  • By device: Mobile vs desktop abandonment rates can differ significantly.
  • By time of day: Abandonment might spike during work hours (customer got distracted).
  • By order value: Higher-value orders often have higher abandonment because the customer needs more time to decide.

Handling Partial Completions

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.

Stay Up to Date

We regularly update our Paystack guides with new patterns for checkout optimization and recovery strategies.

Sign up for the McTaba newsletter to get notified about new Paystack integration guides.

Key Takeaways

  • A transaction is "abandoned" when it was initialized but the customer never completed payment. Paystack tracks this status automatically.
  • Use the Paystack API to list transactions with status=abandoned to find checkouts that need follow-up.
  • Recovery emails work. Send a polite follow-up within an hour of abandonment with a fresh payment link. Many customers abandoned due to distraction, not disinterest.
  • Do not reuse the old access code for recovery. Initialize a new transaction with the same reference to get a fresh checkout link.
  • Track abandonment rates by payment channel. If one channel has high abandonment, it might have usability issues for your audience.
  • The most effective way to reduce abandonment is to show the total cost (including fees and shipping) before the customer clicks "Pay." Surprise costs at checkout are the top abandonment reason globally.

Frequently Asked Questions

How long before Paystack marks a transaction as abandoned?
Paystack marks a transaction as abandoned after the checkout session expires without a payment attempt. The exact timeout varies but is generally within a few hours to 24 hours after initialization. You can also track abandonment on your side by monitoring pending records that are older than your own threshold.
Can I reuse the same reference for a recovery payment?
If the original transaction is abandoned (not completed), you can initialize a new transaction with the same reference and Paystack will return the existing pending transaction with a fresh access code. If the reference was already used for a completed transaction, you need a new reference.
Should I send SMS or email for recovery?
Both work. Email is cheaper and lets you include more detail (product images, order summary). SMS is more immediate and has higher open rates in many African markets. If you have the customer phone number and your margins support SMS costs, a short SMS with a payment link can be very effective.
How do I prevent creating duplicate orders from recovery attempts?
Tie the payment to the original order ID. Whether the customer pays through the original checkout or a recovery link, the order ID in your metadata stays the same. Your fulfillment logic should check if the order has already been fulfilled before processing.
Is there a Paystack webhook for abandoned transactions?
Paystack does not send a webhook specifically for abandoned transactions. You need to detect abandonment by tracking initialized transactions on your end and checking which ones were never completed. The charge.success webhook only fires for successful payments.

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