Tranzak Docs
tranzak.co Dashboard
Docs  /  Accept payments

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 success query parameter is never proof of payment. Never mark an order as paid because the customer came back to your site.

  1. On create. Save { order_id, transaction_id, status: "pending" } in your database before you redirect the customer.
  2. Primary signal. Handle the signed webhook. It tells you as soon as the payment is final.
  3. 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}. Use POST /payments/{transaction_id}/verify to force Tranzak to check the provider.
  4. 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.
  5. Check before you fulfil. Compare data.amount and data.currency with your order, and data.reference with 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.