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.
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.
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).
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
A successful card payment timeline typically looks like this sequence of events:
/transaction/initialize or the customer opened the popup.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.
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.
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.
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.
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.
Test your timeline integration using Paystack's test mode. Create transactions with different outcomes and verify that your support tool displays the correct information.
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.
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