Confirm a payment
The recommended way to confirm a Tranzak payment on your server: webhook first, a read as safety net, and idempotent order updates.
A payment is confirmed only by server-side proof: a signed webhook, or a read of the payment that returns status: "completed".
Warning A browser redirect, a client-side callback or a
successquery parameter is never proof of payment. Never mark an order as paid because the customer came back to your site.
The recommended algorithm
- On create. Save
{ order_id, transaction_id, status: "pending" }in your database before you redirect the customer. - Primary signal. Handle the signed webhook. It tells you as soon as the payment is final.
- Safety net. When the customer returns to your return URL, and on a timer for orders still pending (for example every minute for 20 minutes, then once), read the payment with
GET /payments/{transaction_id}. UsePOST /payments/{transaction_id}/verifyto force Tranzak to check the provider. - Be idempotent. Make "mark the order as paid" safe to run twice: only the first transition to paid has effects such as stock, emails or access.
- Check before you fulfil. Compare
data.amountanddata.currencywith your order, anddata.referencewith your order identifier.
Why a safety net is needed
A transaction left in processing for 15 minutes is automatically marked failed. You may not receive a webhook for an expired transaction, so your code must also reconcile pending orders. NatCash also has no instant notification: Tranzak polls it every 2 minutes.
Read the payment
curl https://api.tranzak.co/api/gateway/v1/payments/17843268616389 \
-H "X-Api-Key: $TRANZAK_API_KEY"
const res = await fetch('https://api.tranzak.co/api/gateway/v1/payments/17843268616389', {
headers: { 'X-Api-Key': process.env.TRANZAK_API_KEY },
});
const { data } = await res.json();
if (data.status === 'completed') {
// Check amount, currency and reference, then fulfil the order once
}
import os, requests
data = requests.get(
"https://api.tranzak.co/api/gateway/v1/payments/17843268616389",
headers={"X-Api-Key": os.environ["TRANZAK_API_KEY"]},
).json()["data"]
if data["status"] == "completed":
pass # Check amount, currency and reference, then fulfil the order once
$ch = curl_init('https://api.tranzak.co/api/gateway/v1/payments/17843268616389');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('TRANZAK_API_KEY')]]);
$data = json_decode(curl_exec($ch), true)['data'];
if ($data['status'] === 'completed') {
// Check amount, currency and reference, then fulfil the order once
}
Branch your logic on data.status only. It is always one of pending, processing, completed or failed.
Compare amounts safely
Amounts are returned as strings, for example "500.00". Compare them as decimals, not as floating-point numbers, and compute the expected amount on your server from your own order data. Never trust an amount sent by the browser.
Checklist
- The transaction identifier is stored before the redirect.
- The order is fulfilled only after server-side confirmation, exactly once.
- Pending orders are reconciled on a timer.
- Amount, currency and reference are checked before fulfilment.