Why the solution was shared
The original post came from a practical engineering problem, not a marketing exercise. After working through a difficult BillDesk integration, an E Multitech developer documented the approach so another PHP developer facing the same opaque message flow would have a clearer starting point. That act of sharing is the history worth retaining.
The useful part was the reasoning: understand the positional request, keep the signing step in one place, distrust a response carried through the browser, and verify it on the server before changing payment state.
The 2014 integration model
In the interface documented in 2014, the merchant application assembled a positional, pipe-delimited transaction message. It added the identifiers and return location issued for that merchant, calculated an HMAC-SHA256 digest with a shared checksum key, and appended the digest to the message. A response later returned through the customer’s browser and had to be verified by the merchant server.
Those details are historical. They describe what the engineer had to solve at the time; they do not assert that BillDesk uses the same fields, endpoints, algorithm, callback transport or status values today.
Illustrative legacy request pattern
The following code is an illustrative legacy pattern, not production-ready integration code. It deliberately contains no live endpoint, merchant value, client reference or secret. Secrets are read only from environment variables; a real deployment should inject them from protected secret storage.
$merchantId = getenv('BILLDESK_MERCHANT_ID');
$securityId = getenv('BILLDESK_SECURITY_ID');
$checksumKey = getenv('BILLDESK_CHECKSUM_KEY');
$gatewayEndpoint = getenv('BILLDESK_GATEWAY_ENDPOINT');
$returnUrl = getenv('BILLDESK_RETURN_URL');
// Historical positional shape, shortened for illustration.
$legacyFields = [
$merchantId,
'[YOUR_ORDER_REFERENCE]',
'[FIELD_UNUSED_IN_LEGACY_PATTERN]',
'[YOUR_TRANSACTION_AMOUNT]',
'[YOUR_CURRENCY_CODE]',
$securityId,
$returnUrl,
];
$legacyBody = implode('|', $legacyFields);
// HMAC-SHA256 reflects the 2014 implementation only.
$legacyChecksum = strtoupper(
hash_hmac('sha256', $legacyBody, $checksumKey, false)
);
$legacyMessage = $legacyBody . '|' . $legacyChecksum;
// Use $gatewayEndpoint only as directed by current provider documentation.
The important historical insight is that positional formats are unforgiving. A missing separator changes the meaning of every field after it, so the message should be assembled from a clearly named structure in one audited server-side component. The checksum provides integrity, not confidentiality: transport security and careful data handling remain separate requirements.
The durable response-verification lesson
The 2014 solution did not trust the payment result merely because a browser posted it back. It separated the received digest from the message, recomputed the digest on the server and compared the two before evaluating the documented transaction status.
$legacyPayload = $_POST['msg'] ?? '';
$lastSeparator = strrpos($legacyPayload, '|');
if ($lastSeparator === false) {
throw new RuntimeException('Invalid legacy response shape');
}
$legacyBody = substr($legacyPayload, 0, $lastSeparator);
$receivedChecksum = substr($legacyPayload, $lastSeparator + 1);
$expectedChecksum = strtoupper(
hash_hmac('sha256', $legacyBody, $checksumKey, false)
);
if (!hash_equals($expectedChecksum, $receivedChecksum)) {
throw new RuntimeException('Response integrity check failed');
}
// Read the status field and success value from current provider documentation.
// Do not fulfil an order from browser-visible data alone.
This remains a useful security principle, but the sample is not a specification. A current implementation must follow the provider’s present verification, reconciliation, replay-protection and failure-handling requirements.
Security note
What we would do differently today
- Start with current documentation. Obtain the latest merchant specification, endpoint information, supported signing method and test credentials directly from BillDesk before writing production code.
- Protect every secret. Keep credentials in managed secret storage and expose them to the smallest possible server-side runtime scope through environment injection or an equivalent protected mechanism.
- Verify on the server. Validate authenticity and integrity before changing payment state, then reconcile against the provider’s authoritative server-side records using the current supported process.
- Log safely. Record correlation identifiers, state transitions and redacted error categories rather than credentials, signatures, full payloads or sensitive customer data.
- Test failure paths. Exercise tampered messages, amount or currency mismatches, duplicate delivery, replay attempts, timeouts, partial failures and reconciliation differences in an isolated test environment.
- Require professional review. Have the provider and qualified payment/security engineers review the final design, operational controls and launch checklist. Our current banking and fintech software engineering work treats this review as part of the transaction boundary.
The durable engineering lesson
The interface may have changed, but the problem-solving record remains valuable. E Multitech’s engineer turned a difficult integration into a reproducible explanation for other developers: centralise message construction, keep secrets out of public artefacts, verify payment evidence on the server, and treat browser-carried state as untrusted. That is the historical contribution this retrospective preserves.