Worked session example
Set the server deadline to 12:00:30. The customer confirms at 12:00:25, but the gateway reply is lost. The ledger posts at 12:00:27. At 12:00:35, the gateway repeats the same command; it cannot create another debit. At 12:01, the customer starts a new session and asks for status. The answer must show the first operation as complete. In a second test, the first confirmation reaches the server at 12:00:31; it must fail because the session has expired. These are synthetic times. Use the actual server clock and gateway delivery timestamps in your run.
Run the timeline
Use a test gateway to delay the reply after the ledger commits. Send the same menu confirmation again before the session ends, after it ends, and from a new session. Compare the final ledger, not just the text shown on the phone.
Checks to run
- Expire the session token after its server-side deadline; reject late confirmation from the old session.
- Replay the same confirmation with the same operation key and confirm that no second debit appears.
- Start a fresh session after timeout and show the prior operation status before offering another transfer.
Make the clock visible
Record when the phone sent confirmation, when the gateway delivered it, when the server accepted it, and when the ledger committed. Inject a late callback after the USSD menu closes. A customer should see the first payment result in a status view rather than a second blank form.
Test both a session nonce and an operation key. The nonce keeps an old menu command from being accepted. The operation key keeps a committed payment from being repeated. They solve different failures.
One more boundary test
Add a menu-step test. A customer reaches the confirmation screen, then changes the recipient in an earlier menu step through a stale gateway message. The server must bind the confirmed amount and recipient to the current session state. A valid code entered for one transfer must not approve a different transfer after a timeout. If the gateway retries an old menu message, the server should show the current step or end the expired session. Keep a trace of session ID, step number, nonce, operation ID, and ledger result.
A session clock test
Use two clocks: the USSD session expiry and the payment operation state. At 11:59:58, submit operation U-01. Let the USSD gateway close at 12:00:00 while the bank still shows pending. At 12:00:03, reconnect and send the same customer action. The server must look up U-01 before creating another debit. Test the case where U-01 later succeeds and the case where it fails. Record the gateway message, bank reference, and ledger count. A fresh USSD session is not proof of a fresh payment intent. Paystack documents unique transfer references for tracking and retrying uncertain transfers.
Distinguish expired commands from completed operations
Use a deadline of 12:00:30 UTC and define acceptance as server arrival strictly before that time. A new command arriving exactly at 12:00:30 is rejected without posting. A replay of U-01, already accepted at 12:00:25, still cannot post again after the deadline. Return its stored result only through an authenticated status path or the gateway protocol’s permitted replay response. Do not reopen the expired session. Test a lost reply after commit separately from a late first request. The first has one operation to recover; the second has none. Bind amount and recipient to the accepted menu step so an old nonce cannot approve a changed transfer.
Primary sources
Next step
Replay a confirmation after the USSD session expires and inspect the ledger before changing the customer message. Read the related guide. For a review of your own system, request a security review.