The Paystack Transaction Timeline API returns a chronological list of events for a single transaction. Each event includes a timestamp, action description, and relevant data. You can see when the transaction was initialized, when the customer entered card details, when authorization was sent to the bank, whether 3DS was triggered, when the charge succeeded or failed, and when the webhook was sent.
The timeline endpoint gives you a step-by-step record of everything that happened during a transaction. You can query it by transaction ID or reference.
// Fetch timeline by transaction reference
const reference = 'order_142_1721472000000';
const response = await fetch(
'https://api.paystack.co/transaction/timeline/' + reference,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const data = await response.json();
// data.data.history is an array of timeline events
// data.data.start_time is when the transaction started
for (const event of data.data.history) {
console.log(event.time + ' | ' + event.type + ' | ' + event.message);
}
Each event in the history array contains:
The timeline is available for all transaction types: card, bank transfer, USSD, and mobile money. The events differ by channel, but the structure is the same.
Here is what a typical successful card payment timeline looks like:
// Example timeline for a successful card payment
[
{ "type": "action", "message": "Attempted to charge card", "time": 0 },
{ "type": "action", "message": "Authentication Required: OTP", "time": 1 },
{ "type": "action", "message": "OTP submitted", "time": 45 },
{ "type": "success", "message": "Transaction Successful", "time": 47 },
{ "type": "action", "message": "Webhook sent to https://yoursite.com/webhooks/paystack", "time": 48 },
{ "type": "action", "message": "Webhook acknowledged (200 OK)", "time": 49 }
]
And here is a failed payment timeline:
// Example timeline for a failed card payment
[
{ "type": "action", "message": "Attempted to charge card", "time": 0 },
{ "type": "action", "message": "Authentication Required: OTP", "time": 1 },
{ "type": "action", "message": "OTP submitted", "time": 120 },
{ "type": "close", "message": "Transaction Failed: Incorrect OTP", "time": 122 }
]
From the first timeline, you can see the payment completed in about 49 seconds, the customer took 45 seconds to enter their OTP, and the webhook was delivered successfully.
From the second timeline, you can see the customer took 2 minutes to enter the OTP and entered the wrong one. The charge failed because of incorrect OTP, not insufficient funds or a card problem.
This level of detail is what separates guesswork from informed debugging.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Here are the most common customer complaints and how to use the timeline to investigate them.
"I was charged but did not receive my product"
Pull the timeline. Check if the transaction succeeded (look for a "Transaction Successful" event). If it did, check the webhook events. Was the webhook sent? Was it acknowledged with a 200 status? If the webhook was not acknowledged, your server did not process the payment notification. Check your webhook handler logs.
"My payment failed but I was debited"
Pull the timeline. Look at the last event. If it says "Transaction Failed" with a specific reason, the charge was declined by the bank. Some banks show a temporary debit that gets reversed within 24 to 48 hours. Explain this to the customer. If the timeline shows "Transaction Successful" but your system shows it as failed, the issue is in your verification or webhook handling.
"I was charged twice"
Check your database for multiple transactions from the same customer around the same time. Pull the timeline for each transaction reference. If both timelines show "Transaction Successful," the customer may have clicked the pay button twice and initiated two separate charges. Check whether both references were generated by your system (a double-submit bug) or if one was a retry initiated by the customer.
"The payment just hung and nothing happened"
Pull the timeline. If the last event is "Authentication Required: OTP" with no subsequent events, the customer never entered the OTP. The bank sent the OTP but the customer did not complete the step. The transaction will eventually time out and be marked as abandoned.
Instead of having support agents ask engineers to look up transaction details, build a simple internal tool that anyone on the support team can use.
// Simple Express endpoint for your internal admin panel
app.get('/admin/transaction/:reference', async (req, res) => {
const reference = req.params.reference;
// Fetch transaction details
const txResponse = await fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const txData = await txResponse.json();
// Fetch timeline
const tlResponse = await fetch(
'https://api.paystack.co/transaction/timeline/' + reference,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const tlData = await tlResponse.json();
// Fetch order from our database
const order = await db.query(
'SELECT * FROM orders WHERE paystack_reference = $1',
[reference]
);
res.json({
transaction: {
reference: txData.data.reference,
amount: txData.data.amount / 100,
currency: txData.data.currency,
status: txData.data.status,
channel: txData.data.channel,
customer_email: txData.data.customer.email,
gateway_response: txData.data.gateway_response,
paid_at: txData.data.paid_at,
},
timeline: tlData.data.history.map(function(e) {
return {
time_seconds: e.time,
type: e.type,
message: e.message,
};
}),
order: order.rows[0] || null,
});
});
This endpoint combines three data sources into one view: the Paystack transaction details, the timeline events, and your internal order record. A support agent enters the reference, sees everything in one place, and can resolve the customer's issue without engineering help.
Add this to your admin dashboard with a search bar. The agent types a reference or customer email, and the tool displays the transaction history and timeline.
Webhook problems are one of the most common integration issues. The timeline helps you pinpoint exactly what happened.
Scenario: Webhook was sent but not acknowledged
The timeline shows "Webhook sent to [your URL]" followed by "Webhook failed (timeout)" or "Webhook failed (500)." This means Paystack sent the webhook, but your server did not respond correctly. Check your server logs around the timestamp for errors.
Scenario: No webhook event in the timeline
If the transaction succeeded but the timeline does not show a webhook event, the webhook may not be configured correctly on your Paystack dashboard. Check Settings > API Keys & Webhooks in your Paystack dashboard to verify the webhook URL.
Scenario: Webhook acknowledged but order not fulfilled
The timeline shows "Webhook acknowledged (200 OK)" but the customer did not receive their product. This means Paystack delivered the webhook and your server returned 200, but your webhook handler did not fulfill the order. The bug is in your code, not in Paystack. Check your webhook handler logic: is it verifying the transaction? Is it checking the amount and status? Is it updating the order in the database?
For each webhook issue pattern, the timeline tells you whether the problem is on Paystack's side (webhook not sent), your network's side (webhook not received), or your application's side (webhook received but not processed correctly).
For high-volume applications, store timeline data for failed transactions automatically. This gives you a dataset for analyzing failure patterns without having to query Paystack on demand.
// In your webhook handler, store the timeline for failed transactions
async function onTransactionFailed(reference) {
try {
const response = await fetch(
'https://api.paystack.co/transaction/timeline/' + reference,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const data = await response.json();
await db.query(
'INSERT INTO transaction_timelines (reference, timeline_data, created_at) VALUES ($1, $2, $3)',
[reference, JSON.stringify(data.data.history), new Date()]
);
} catch (error) {
console.log('Failed to store timeline for ' + reference + ': ' + error.message);
}
}
Over time, this data reveals patterns. You might discover that 40% of your failed payments fail at the OTP step (customers not receiving or entering the OTP). That insight leads you to add clearer OTP instructions in your checkout flow. Or you might find that failures spike at certain times of day, pointing to bank-side processing issues.
For the full accept payments workflow, see the complete accept payments guide. For understanding bank decline reasons, see reading gateway responses.
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