Verifying Payment Status

ជំពូកនេះមិនទាន់ត្រូវបានបកប្រែជាភាសាខ្មែរនៅឡើយទេ — សូមអានជាភាសាអង់គ្លេសខាងក្រោម។

Table of Contents

  1. Why this step exists
  2. The check-transaction hash
  3. Polling from the frontend
  4. Beginner Pitfalls

1. Why this step exists

This is PayWay's documented Step 2, "Verify Payment Status" (official reference) — the part a lot of beginner guides skip. A code: "00" response from Part 3's purchase call only means PayWay accepted the request; it says nothing about whether the customer actually paid. You confirm the real outcome by polling a second endpoint with the tran_id from Part 3.

Don't skip this because you're also planning the callback (Part 5). They serve different purposes — polling gives your frontend a synchronous way to show a result to the customer right now; the callback is the asynchronous, server-to-server source of truth you should ultimately rely on for updating your database. A production integration typically uses both.

2. The check-transaction hash

function utcTimestamp() {
  return new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
}

router.post('/check-payment-status', async (req, res) => {
  const { tran_id } = req.body;
  const req_time = utcTimestamp(); // UTC 'YYYYMMDDHHmmss' — NOT unix seconds

  const stringToHash = `${req_time}${MERCHANT_ID}${tran_id}`;
  const hash = crypto.createHmac('sha512', PUBLIC_KEY).update(stringToHash).digest('base64');

  const paywayRes = await fetch(CHECK_TRANSACTION_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ req_time, merchant_id: MERCHANT_ID, tran_id, hash }),
  });
  res.json(await paywayRes.json());
});

Confirmed against PayWay's Check Transaction API docs: the hash is HMAC-SHA512(req_time + merchant_id + tran_id), base64-encoded, signed with the same Public Key as the purchase call in Part 3 — but a different req_time format. The purchase call uses unix seconds; this call uses UTC YYYYMMDDHHmmss (e.g. 20260816143000). Reusing the wrong format produces a wrong hash, not a clear error.

The response's key fields are data.payment_status — confirmed live against the sandbox: uppercase values like PENDING, APPROVED, DECLINED, CANCELLED, REFUNDED — and the top-level status.code ("00" means the status check itself succeeded, separate from payment_status). Note the status fields are nested under data, not top-level — easy to miss.

PayWay documents a rate limit of 600 requests/second for this endpoint and asks integrators to stop polling once a result is returned — don't poll forever, and don't poll faster than needed.

3. Polling from the frontend

async function pollPaymentStatus(tran_id) {
  const POLL_INTERVAL_MS = 3000;
  const MAX_ATTEMPTS = 20; // ~1 minute for a teaching example

  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));

    const res = await fetch('/check-payment-status', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tran_id }),
    });
    const data = await res.json();

    const paymentStatus = data.data?.payment_status;
    if (paymentStatus && paymentStatus !== 'PENDING') {
      return paymentStatus; // show this to the customer
    }
  }
  // still pending after MAX_ATTEMPTS — let the customer refresh
}

Kick this off right after showing the QR code / deep link from Part 3, and stop as soon as payment_status leaves PENDING.

4. Beginner Pitfalls

  • Don't reuse the purchase call's req_time format here — this endpoint wants UTC YYYYMMDDHHmmss, not unix seconds. A wrong format produces a wrong hash with no obviously-related error message.
  • The status fields are nested under data.data/response.data depending on how you name your variables — check the nesting yourself rather than assuming payment_status is top-level.
  • Treating this step as skippable because you also have a callback is the single most common mistake here — see the note in Section 1.

ទិញកាហ្វេជូនខ្ញុំ

ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!