Debugging Paystack with the Transaction Timeline
The Paystack Transaction Timeline API (GET /transaction/timeline/:id_or_reference) returns a chronological list of every event in a transaction: initialization, card input, bank authorization, OTP entry, charge attempt, success or failure, and webhook delivery. Fetch it with your secret key and read the events array to pinpoint exactly where a payment failed. This is the single fastest way to debug customer payment complaints.
What the Transaction Timeline Shows You
The Transaction Timeline is a chronological log of every event that happened during a single Paystack transaction. Think of it as the receipt tape from the payment processor's perspective. It records the moment the transaction was initialized, when the customer entered their card details, when the bank received the authorization request, whether an OTP was required, the bank's response, and whether the webhook was delivered.
Each event in the timeline has a type (like "input", "action", "success", or "close"), a message describing what happened, and a timestamp. Reading these events in order tells you the exact story of the transaction.
This matters because the standard Verify Transaction endpoint only gives you the final result: success, failed, or abandoned. It does not tell you where the failure occurred. A payment that failed because the customer closed the popup is a completely different problem from one that failed because the bank declined the card. The timeline distinguishes between these cases.
Fetching the Timeline via API
The endpoint is straightforward. Send a GET request to /transaction/timeline/:id_or_reference with your secret key in the Authorization header.
// Node.js example: fetch transaction timeline
async function getTransactionTimeline(idOrReference) {
const response = await fetch(
`https://api.paystack.co/transaction/timeline/${idOrReference}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
const data = await response.json();
if (!data.status) {
throw new Error(`Timeline fetch failed: ${data.message}`);
}
return data.data;
}
// Usage with transaction reference
const timeline = await getTransactionTimeline('order_abc123_1721500000');
console.log('Events:', timeline.history);
console.log('Total time:', timeline.time_spent, 'seconds');
You can pass either the numeric transaction ID (from the Paystack response) or the reference string you generated when initializing the transaction. Using the reference is usually more convenient because that is what your system stores.
The response includes a history array of events and a time_spent field showing the total seconds from initialization to completion (or abandonment).
Reading Timeline Events Step by Step
A successful card payment timeline typically looks like this sequence of events:
- "Transaction initialized" - Your server called
/transaction/initializeor the customer opened the popup. - "Filled in card details" - The customer entered their card number, expiry, and CVV on the Paystack checkout.
- "Attempted to pay" - Paystack sent the card details to the acquiring bank for authorization.
- "OTP was sent to customer" - The issuing bank required 3D Secure verification and sent an OTP to the customer's phone.
- "Customer filled in OTP" - The customer entered the OTP on the checkout page.
- "Transaction was successful" - The bank authorized the charge and the funds were captured.
Not every transaction has all these steps. A card that does not require OTP skips steps 4 and 5. A bank transfer transaction has different events entirely (account assigned, transfer received). Mobile money transactions show USSD or STK push events.
The key debugging insight is always: what was the last event? If the timeline stops at "Filled in card details" without an "Attempted to pay" event, Paystack's fraud system may have blocked the attempt. If it stops at "OTP was sent" without "Customer filled in OTP," the customer did not complete verification.
Common Failure Patterns in the Timeline
Pattern 1: Customer abandoned at card input. Timeline shows "Transaction initialized" and nothing else, or "Filled in card details" followed by "Transaction closed by customer." The customer opened the checkout, possibly entered card details, then closed the window. This is not a technical error. It is a UX or trust problem.
Pattern 2: Bank declined the card. Timeline shows events up through "Attempted to pay" then a failure event with a gateway_response like "Declined" or "Insufficient Funds." The card was sent to the bank and rejected. Check the gateway_response field for the specific reason.
Pattern 3: OTP timeout. Timeline shows "OTP was sent to customer" but no subsequent OTP entry event. The customer either did not receive the OTP (network issue, wrong phone number on file with the bank) or received it but did not enter it before the timeout window closed. Common with Nigerian bank cards.
Pattern 4: 3DS redirect failure. Some banks redirect the customer to their own page for verification instead of using an inline OTP. If the timeline shows "Customer was redirected to the bank" with no return event, the customer got stuck on the bank's 3DS page. This is especially common with international cards.
Pattern 5: Paystack internal timeout. Timeline shows "Attempted to pay" with no response event for an extended period, then a timeout. The bank's authorization system was too slow. This happens during peak hours or bank maintenance windows. The customer was not charged.
Pattern 6: Webhook delivery failure. The timeline may show the transaction succeeded but the webhook was not delivered or returned a non-200 status. This explains cases where the payment went through but your system did not update. Look for "Webhook sent" or "Webhook failed" events.
Building a Support Lookup Tool
Instead of having your support team log into the Paystack Dashboard for every customer complaint, build an internal tool that fetches the timeline automatically.
// Express.js: internal support endpoint
app.get('/admin/support/payment-trace/:reference', requireAdmin, async (req, res) => {
const { reference } = req.params;
// Fetch both transaction details and timeline in parallel
const [txResponse, timelineResponse] = await Promise.all([
fetch(`https://api.paystack.co/transaction/verify/${reference}`, {
headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` },
}),
fetch(`https://api.paystack.co/transaction/timeline/${reference}`, {
headers: { Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}` },
}),
]);
const [txData, timelineData] = await Promise.all([
txResponse.json(),
timelineResponse.json(),
]);
// Combine into a single support view
const supportView = {
reference,
status: txData.data?.status,
amount: txData.data?.amount / 100, // Convert from kobo/pesewas
currency: txData.data?.currency,
channel: txData.data?.channel,
gatewayResponse: txData.data?.gateway_response,
paidAt: txData.data?.paid_at,
timeline: timelineData.data?.history || [],
totalTimeSeconds: timelineData.data?.time_spent,
diagnosis: diagnoseTimeline(timelineData.data?.history || []),
};
res.json(supportView);
});
function diagnoseTimeline(events) {
if (events.length === 0) return 'No timeline events. Transaction may not have reached Paystack.';
const lastEvent = events[events.length - 1];
const eventMessages = events.map(e => e.message?.toLowerCase() || '');
if (eventMessages.some(m => m.includes('successful'))) {
return 'Transaction succeeded. Check webhook delivery and your order fulfillment logic.';
}
if (eventMessages.some(m => m.includes('closed by customer'))) {
return 'Customer closed the checkout window before completing payment.';
}
if (eventMessages.some(m => m.includes('insufficient'))) {
return 'Bank declined: insufficient funds. Customer needs a different card or to fund their account.';
}
if (eventMessages.some(m => m.includes('otp')) && !eventMessages.some(m => m.includes('filled in otp'))) {
return 'OTP was sent but customer did not enter it. Possible timeout or SMS delivery failure.';
}
return `Last event: ${lastEvent.message}. Review full timeline for details.`;
}
This gives your support team a one-click view of what happened, without needing Paystack Dashboard access. The diagnoseTimeline function provides a plain-English summary that non-technical support agents can read directly to the customer.
Timeline API vs Verify Transaction
These two endpoints serve different purposes and you need both in a production system.
Verify Transaction (GET /transaction/verify/:reference) tells you the final result: did the payment succeed, fail, or get abandoned? It also gives you the amount, currency, payment channel, and gateway response. This is what your webhook handler and callback page should use to confirm payment status.
Transaction Timeline (GET /transaction/timeline/:id_or_reference) tells you the story of how the transaction reached that result. It does not give you the amount or status directly. It gives you the sequence of events.
Use Verify for your payment confirmation logic. Use Timeline for debugging when something goes wrong. A common mistake is trying to use the Timeline as a substitute for Verify. It is not. The Timeline is a diagnostic tool, not a source of truth for payment status.
Rate limits apply to both endpoints. If you are fetching timelines for many transactions in bulk (for a reconciliation report, for example), throttle your requests to avoid hitting the API rate limit. One request per second is a safe pace for bulk lookups.
Storing Timeline Data for Historical Analysis
Timeline data is available from Paystack's API for a limited period. If you want to analyze failure patterns over months, store the timeline data in your own database when issues occur.
// After detecting a failed or disputed transaction, store its timeline
async function storeTimelineForFailedTransaction(reference, transactionId) {
try {
const timeline = await getTransactionTimeline(reference);
await db.query(
`INSERT INTO payment_timelines (transaction_reference, transaction_id, events, time_spent, fetched_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (transaction_reference) DO NOTHING`,
[reference, transactionId, JSON.stringify(timeline.history), timeline.time_spent]
);
} catch (error) {
// Log but do not throw. Timeline storage is diagnostic, not critical.
console.error(`Failed to store timeline for ${reference}:`, error.message);
}
}
With stored timelines, you can run queries like "what percentage of failures last month were OTP timeouts?" or "which bank causes the most 3DS redirect failures?" This data helps you make informed decisions about which payment channels to promote or which customer messaging to show for specific failure types.
Verifying Your Timeline Integration Works
Test your timeline integration using Paystack's test mode. Create transactions with different outcomes and verify that your support tool displays the correct information.
- Successful payment: Complete a test transaction with Paystack's test card (4084 0840 8408 4081, any future expiry, any CVV, OTP 123456). Fetch the timeline and confirm you see all events from initialization through success.
- Failed payment: Use a test card that triggers a decline. Fetch the timeline and verify your diagnosis function returns a helpful message.
- Abandoned payment: Initialize a transaction but do not complete it. After the timeout, fetch the timeline and confirm it shows the abandonment.
- Check error handling: Fetch a timeline for a reference that does not exist. Confirm your code handles the error gracefully instead of crashing.
Run these tests whenever you update your support tool or change your payment flow. Timeline event messages can change when Paystack updates their checkout experience, so your diagnosis function may need adjustments over time.
Key Takeaways
- ✓The Transaction Timeline API returns every event in a payment, in chronological order. It shows initialization, customer input, bank authorization, OTP, charge result, and webhook delivery.
- ✓Fetch timelines using GET /transaction/timeline/:id_or_reference with your secret key. You can use either the transaction ID or the reference string.
- ✓The most common debugging pattern is finding the last event before failure. If the timeline stops at "input" the customer abandoned. If it stops at "action" the bank never responded. If it shows "failed" with a gateway_response, the bank declined.
- ✓Build a support tool that automatically fetches the timeline when a customer reports a payment issue. This saves your support team from logging into the Paystack Dashboard every time.
- ✓Timeline data is available for at least 90 days. For older transactions, you will need to rely on your own logs.
- ✓Never expose timeline data to customers or frontend code. It contains internal processing details and is fetched with your secret key.
Frequently Asked Questions
- How long is Transaction Timeline data available from the Paystack API?
- Paystack retains timeline data for at least 90 days after the transaction date. For transactions older than this, you will need to rely on your own stored logs. If long-term timeline analysis is important to your business, store the timeline data in your database when you fetch it.
- Can I fetch timelines for test mode transactions?
- Yes. The Timeline API works identically in test mode and live mode. Use your test secret key and test transaction references. This is useful for building and testing your support tool before going live.
- Does the timeline show webhook delivery status?
- The timeline may include events related to webhook delivery, showing whether the webhook was sent and what response your server returned. However, for detailed webhook delivery logs, the Paystack Dashboard webhook logs section provides more complete information including retry attempts.
- Can I use the Transaction Timeline to detect fraud?
- The timeline shows behavioral signals like how quickly a customer entered card details (unusually fast input may indicate automated card testing) and whether multiple failed attempts preceded a success. These patterns can supplement your fraud detection, but they are not a replacement for proper fraud monitoring tools.
- Is there a rate limit on the Timeline API?
- Yes, the same rate limits that apply to other Paystack API endpoints apply here. If you are fetching timelines for many transactions at once (for a batch report or reconciliation job), throttle your requests to one per second to stay well within limits. For individual support lookups, rate limits are not a concern.
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