Verify callbacks and checksums
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.
Callback URL and return URL
Section titled “Callback URL and return URL”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.
Callbacks Bayarcash sends
Section titled “Callbacks Bayarcash sends”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 |
Answer every callback with HTTP 200
Section titled “Answer every callback with HTTP 200”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.
How the checksum works
Section titled “How the checksum works”The same rule signs requests and verifies callbacks. Only the list of fields changes from call to call. To build a checksum:
- 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.
- Sort the fields by field name, A to Z.
- Join the values, in that order, with a
|between each. - 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.
Fields in each checksum
Section titled “Fields in each checksum”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);Example: sign a payment intent in Python
Section titled “Example: sign a payment intent in Python”The same rule in Python, for a payment intent request:
import hashlibimport 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?
Thanks for your feedback.
Sorry this didn't help. WhatsApp us and we'll sort it out with you.

