Prevent duplicate airtime and bill payment purchases
Use durable order references and reconciliation to prevent duplicate purchases when a request times out, a customer retries or a worker runs twice.
Already registered? Open API settings and follow the business-access steps. Sandbox purchases do not deliver real services.
Reviewed
Duplicate prevention begins in your own order system. Disabling a Pay button helps the interface, but it cannot stop a repeated HTTP request, a queue retry or a second worker. Keep one stable order identity across all of them.
Save one reference for one order
RizPay documents external_reference as ten Unix timestamp digits followed by six alphanumeric characters. Generate that reference on your server and persist it with the order before the first purchase request. Enforce uniqueness in your database and handle the unlikely collision by generating another value before any request is sent.
Do not generate the reference inside a retry loop. If each attempt gets a different reference, the requests look like different purchases. The customer's intentional next purchase should get a new order; a retry of the existing order should not.
A duplicate error is not a delivery receipt
DUPLICATE_REFERENCE means the reference has already been used. It does not tell you whether the associated purchase is pending, successful, failed or reversed. Treating that error as unconditional success can show airtime as delivered when it is still unresolved.
Retrieve the existing record through /account/transactions/YOUR_STORED_REFERENCE. If you already have the transaction ID, use /purchases/:id. Compare the returned transaction with the local order and reconcile its actual status.
Control concurrency locally
Use a database transaction, row lock or equivalent durable mechanism to ensure only one worker starts the submission step for an order. A process-local mutex does not cover a second application instance. Record that submission began and record the returned transaction ID when available.
Do not hold an ordinary database transaction open indefinitely across a slow provider request. Design an explicit submitting or unresolved state with recovery after worker failure. The recovery worker should inspect the existing reference rather than blindly creating a new request.
| Event | Same local order? | New reference? |
|---|---|---|
| Customer refreshes a pending receipt | Yes | No |
| Worker receives a duplicate job | Yes | No |
| Purchase request times out | Yes | No |
| Customer deliberately confirms another purchase | No | Yes |
Handle old references carefully
The duplicate-prevention documentation includes a same-day reference constraint. If an unresolved order crosses a date boundary, do not rewrite its reference just to pass validation. Reconcile the old order and use support if its outcome remains uncertain. A new reference is not a safe repair for an unknown payment result.
Test failure at the boundary
Exercise a lost create response, two concurrent submissions, a duplicate queue delivery and a restart before the response is saved. Confirm that the customer sees one order and that no code path converts a duplicate error into a delivery claim. Separately deduplicate webhook effects, so a repeated event cannot add a second credit or refund.