Retries & reliability
The result callback is sent once, on a best-effort basis, when a transaction reaches a terminal status.
Build for a lost callback
Section titled “Build for a lost callback”-
Return
2xxfast. Acknowledge the callback, then do fulfilment, emails, and invoicing asynchronously. A handler that takes seconds is a handler that times out. -
Reconcile on a schedule. Run a job that finds every order still awaiting payment after a reasonable window — a few minutes is usually enough — and calls Check Payment Status for each one. This is what catches the transactions whose callback never landed.
-
Stop only on a terminal status. Keep checking until
transactionStatusis terminal.PENDING,PROCESSING, andPARTIALLY_PAIDall mean the payment is still in flight. -
Age out abandoned attempts. Customers walk away from checkouts. Give an unpaid transaction a sensible expiry in your own system rather than polling it forever.
-
Keep the handler idempotent. Your callback handler and your reconciliation job will sometimes process the same result. Key both on
referenceNumber— see Verifying callbacks.
Deploys and downtime
Section titled “Deploys and downtime”Because there’s no retry, a callback that arrives mid-deploy is simply gone. Two things make that a non-event: reconciliation (step 2), and never treating “no callback” as “no payment”. Anything you’d have done from the callback should be reachable from the reconciliation path too.
The reconciliation job
Section titled “The reconciliation job”Run this on a schedule. It calls the same settle function as your callback handler, so a payment settles exactly once whichever path finds it first.
// Every 5 minutes.async function reconcile() { const stale = await orders.findUnpaidOlderThan({ minutes: 5 });
for (const order of stale) { const result = await pesepay.checkPayment(order.referenceNumber); if (!result.success) continue; // transient — try again next run
if (result.paid) { await orders.markPaid(order.id); await fulfil(order); } else if (order.createdAt < hoursAgo(24)) { await orders.expire(order.id); // the customer never came back } }}# Every 5 minutes.def reconcile(): for order in orders.find_unpaid_older_than(minutes=5): result = pesepay.check_payment(order.reference_number) if not result.success: continue # transient — try again next run
if result.paid: orders.mark_paid(order.id) fulfil(order) elif order.created_at < hours_ago(24): orders.expire(order.id) # the customer never came back<?php// Every 5 minutes.foreach (find_unpaid_orders_older_than('5 minutes') as $order) { $response = $pesepay->checkPayment($order['reference_number']); if (!$response->success()) { continue; // transient — try again next run }
if ($response->paid()) { mark_order_paid($order['id']); fulfil_order($order); } elseif (strtotime($order['created_at']) < strtotime('-24 hours')) { expire_order($order['id']); // the customer never came back }}