Skip to content

Popular searches

Verify callbacks and checksums

Updated

When a payment finishes, Bayarcash tells your server with a callback and sends your customer back with a return URL. Every callback carries a checksum: check it with your API Secret Key before you trust the data. You can also add a checksum to your own requests so Bayarcash knows they were not changed.

You set both URLs on each payment intent (callback_url and return_url). They do different jobs:

Callback URL Return URL
Who calls it Bayarcash’s server, directly to yours Your customer’s browser, redirected by Bayarcash
How it arrives POST GET with the fields in the query string (API v3)
Can you trust it for the order status? Yes, after checking the checksum No: the customer can close the tab or change the URL
Use it to Mark the order paid or failed Show the customer a result page

Update your orders from the callback only. If the return URL arrives first, show a “payment is being confirmed” message and wait for the callback, or look the transaction up with the API.

Each callback has a record_type field that tells you what it is:

Callback When it is sent
Pre-transaction Early in the payment, before the transaction record is final.
Transaction When a payment reaches a final state (successful, failed, cancelled and so on).
FPX Direct Debit authorization When a customer completes or abandons e-mandate enrolment.
FPX Direct Debit bank approval When the customer’s bank approves or rejects a mandate. This can come days later.
FPX Direct Debit transaction For each collection taken from an active mandate.

The status field in a transaction callback is a number:

Status Meaning
0 New
1 Pending
2 Unsuccessful
3 Successful
4 Cancelled
5 Abandoned

Your callback URL must answer HTTP 200 once it has checked and saved the result. If it answers with any other status, or does not answer, Bayarcash sends the callback again. So your handler must be safe to run twice: check whether you have already processed that transaction_id before you fulfil the order.

Callbacks are sent as a POST. The Bayarcash plugins and SDKs read them as form fields ($_POST in PHP). If your framework receives application/json, read the JSON body instead.

The same rule signs requests and verifies callbacks. Only the list of fields changes from call to call. To build a checksum:

  1. Take the fields that belong to that call (see the table below), with their values exactly as sent or received. Use an empty value for a field that is missing.
  2. Sort the fields by field name, A to Z.
  3. Join the values, in that order, with a | between each.
  4. Compute an HMAC-SHA256 of that text, using your API Secret Key as the key. The result, as lowercase hex, is the checksum.

For a callback, compare your result with the checksum field the callback carries. Use a constant-time comparison, such as hash_equals() in PHP or hmac.compare_digest() in Python. If they differ, reject the callback.

A checksum on a request is optional but recommended. A checksum on a callback is always there: always verify it.

Each call uses its own fields:

Call Fields in the checksum
Create payment intent (request) payment_channel, order_number, amount, payer_name, payer_email
FPX Direct Debit enrolment (request) order_number, amount, payer_name, payer_email, payer_telephone_number, payer_id_type, payer_id, application_reason, frequency_mode
Pre-transaction callback record_type, exchange_reference_number, order_number
Transaction callback record_type, transaction_id, exchange_reference_number, exchange_transaction_id, order_number, currency, amount, payer_name, payer_email, payer_bank_name, status, status_description, datetime
Return URL transaction_id, exchange_reference_number, exchange_transaction_id, order_number, currency, amount, payer_bank_name, status, status_description
FPX Direct Debit bank approval callback record_type, approval_date, approval_status, mandate_id, mandate_reference_number, order_number, payer_bank_code_hashed, payer_bank_code, payer_bank_account_no, application_type
FPX Direct Debit transaction callback record_type, batch_number, mandate_id, mandate_reference_number, transaction_id, datetime, reference_number, amount, status, status_description, cycle

The table lists the fields; the sort in step 2 decides the order. If a payment intent has more than one payment_channel, join the channel numbers with commas (1,5) before you add it, as the PHP SDK does. For FPX Direct Debit maintenance requests and authorization callbacks, use the verification methods in an official SDK, which carry the exact field lists.

Example: verify a transaction callback in PHP

Section titled “Example: verify a transaction callback in PHP”

This checks a transaction callback without the SDK. With the PHP SDK, verifyTransactionCallbackData() does the same in one line.

function bayarcash_checksum(array $data, array $fields, string $secretKey): string
{
$payload = [];
foreach ($fields as $field) {
$payload[$field] = $data[$field] ?? '';
}
ksort($payload);
return hash_hmac('sha256', implode('|', $payload), $secretKey);
}
$fields = [
'record_type', 'transaction_id', 'exchange_reference_number',
'exchange_transaction_id', 'order_number', 'currency', 'amount',
'payer_name', 'payer_email', 'payer_bank_name', 'status',
'status_description', 'datetime',
];
$callback = $_POST;
$expected = bayarcash_checksum($callback, $fields, getenv('BAYARCASH_API_SECRET_KEY'));
if (! hash_equals($expected, (string) ($callback['checksum'] ?? ''))) {
http_response_code(403);
exit;
}
// Checksum is valid: save the result, then answer 200.
http_response_code(200);

The same rule in Python, for a payment intent request:

import hashlib
import hmac
def bayarcash_checksum(values: dict, secret_key: str) -> str:
joined = "|".join(str(values[k]) for k in sorted(values))
return hmac.new(secret_key.encode(), joined.encode(), hashlib.sha256).hexdigest()
checksum = bayarcash_checksum({
"payment_channel": "1",
"order_number": "INV-1001",
"amount": "10.00",
"payer_name": "Ahmad bin Abdullah",
"payer_email": "ahmad@example.com",
}, "YOUR_API_SECRET_KEY")

Send the result as checksum with the other fields. The full field reference is in the API reference checksum guide.

  • Use HTTPS for your callback and return URLs. The callback carries payment details, and HTTPS keeps them private in transit.
  • Keep the API Secret Key on your server only. Anyone with it can forge a callback that passes the checksum check.
  • Use your framework package if you have one. The Laravel and Django packages add a callback route that checks the checksum and rejects a bad one with HTTP 403.

Common issues

My WooCommerce log says "Invalid checksum for order". What is wrong?

The checksum did not match. Most often the API Secret Key is wrong, or it comes from the other environment (a sandbox key on a production callback, or the reverse). Copy the key again from the merchant dashboard you are using.

My checksum never matches, even with the right key. What should I check?

Use the exact field list for that call, sort by field name, and use the values exactly as sent or received. 10.00 and 10 give different checksums. A missing field counts as an empty value, not as a skipped one.

Why did the same callback arrive twice?

Bayarcash sends a callback again when your server does not answer with HTTP 200. Answer 200 once you have saved the result, and check the transaction ID so you do not process the same payment twice.

Was this article helpful?

Cookie settings

We use Google Analytics to see which guides help and where readers get stuck. It is on by default, and you can turn it off. This device keeps your choice.