Checksum Authentication Guide
Generate and verify HMAC-SHA256 checksums for API requests and callbacks
To guarantee data integrity and authenticity for all communications, Exirom implements a checksum verification mechanism using the HMAC-SHA256 algorithm with your merchantSecret.
This checksum ensures:
- The signed request/callback fields have not been altered in transit.
- The sender is genuinely authenticated using the shared secret.
#When to Use the Checksum
| Direction | Location | Fields used, in order |
|---|---|---|
| APM request to Exirom | Body field (checksum) | accountId → amount → currency → requestId |
| Callback from Exirom | HTTP header (X-Checksum) | See the callback matrix below |
#Callback Checksum Matrix
| Callback | Fields, in order |
|---|---|
| APM payment | accountId → orderAmount → orderCurrency → transactionId |
| Card payment, card payout, APM payout | transactionId → requestId → transactionStatus |
| Card refund | transactionId (original card-payment ID) → requestId → transactionStatus |
The APM-payment callback contract is unchanged. Select the formula from the API flow that originated the callback; do not infer APM payment versus APM payout from the presence of accountId.
For card refunds, transactionId is the original card-payment transaction ID, not refundId. The signed requestId identifies the individual refund. refundId remains available in the callback payload but is not part of the checksum.
#How to Generate the Checksum
#From Merchant to Exirom (Request)
Use the following fields in this exact order as strings:
accountId | amount | currency | requestId
#From Exirom to Merchant (Callback)
For APM payments, use the existing fields in this exact order as strings:
accountId | orderAmount | orderCurrency | transactionId
For card payments, card payouts, card refunds, and APM payouts, use:
transactionId | requestId | transactionStatus
#Apply HMAC-SHA256
- Use the pipe character (
|) as a delimiter. - Sign the concatenated string using HMAC-SHA256 with your
merchantSecret. - Encode the resulting bytes with standard Base64. Do not use hexadecimal or URL-safe Base64.
#Example: Request
Data:
accountId: merchant_001
amount: 10.00
currency: USD
requestId: req-789123
Checksum string:
merchant_001|10.00|USD|req-789123
Send in body:
{
"accountId": "merchant_001",
"amount": "10.00",
"currency": "USD",
"requestId": "req-789123",
"checksum": "<Base64EncodedChecksum>"
}#Example: APM Payment Callback
Data:
accountId: merchant_001
orderAmount: 200.0
orderCurrency: USD
transactionId: tx-456789
Checksum string:
merchant_001|200.0|USD|tx-456789
Sent as header:
X-Checksum: SVch3qOkS+Kp3J1BZgKpQcx4jisvY/ZsejVV8bQL720=
The checksum above uses test-secret. This APM-payment formula remains unchanged.
#Example: Card Payment, Card Payout, Card Refund, or APM Payout Callback
Secret: test-secret
Data:
transactionId: tx_1001
requestId: req_2001
transactionStatus: SUCCEED
Checksum string:
tx_1001|req_2001|SUCCEED
Sent as header:
X-Checksum: 8IlkEoKYhsQmFv+M41TH8Z8SmBCQf41cZpZBCzIYPfY=
The +, /, and = characters are expected standard-Base64 output and must not be replaced or removed.
#Card Refund Identity Example
For a card-refund callback, transactionId contains the original card-payment ID. refundId is present for refund reconciliation but is not signed.
Secret: test-secret
Callback data:
transactionId: payment_1001
refundId: refund_3001 (not signed)
requestId: refund_request_2001
transactionStatus: SUCCEED
Checksum string:
payment_1001|refund_request_2001|SUCCEED
Sent as header:
X-Checksum: /BFiJrrWikPLUdHEWjCRssGHROiKy5U68NxFqjmc/d0=
#APM Amount Formatting
This section applies only to APM requests and APM-payment callbacks. The four transaction-result callback types do not sign amount or currency.
For an APM request, use the exact decimal string as it appears in the request body — no conversion needed.
#✅ Correct Format
| Currency | Request amount | amount for checksum |
|---|---|---|
| USD | "10.00" | "10.00" |
| EUR | "25.50" | "25.50" |
| GBP | "99.99" | "99.99" |
| JPY | "500" | "500" |
| ILS | "3.75" | "3.75" |
💡 Use the same string value in the checksum as you send in the request body — no multiplication or unit conversion required.
🚧 Important – Amount Type and Checksum Validation
The signed amount uses different numeric representations depending on the message direction:
- Requests:
amountis sent as a string (e.g."200.00")- APM-payment callbacks:
orderAmountis returned as a numeric value (e.g.200.0)Exirom processes amounts using native numeric types. As a result, decimal formatting (such as trailing zeros) is not preserved in callbacks.
Example:
- Request:
"200.00"- Callback:
200.0When validating an APM-payment callback checksum, always use the exact
orderAmountrepresentation received in the callback payload.Do not reuse or reformat the original request amount, as this may cause checksum mismatches.
#Validation Rules
Requests without a valid checksum field are rejected with 400 BadRequest. Ignore callbacks with a missing or invalid X-Checksum header.
For card payment, card payout, card refund, and APM payout callbacks, the signature protects only transactionId, requestId, and transactionStatus. Amount, currency, and all other payload fields are intentionally unsigned. Validate those fields separately when your business logic uses them.
For card refunds, deduplicate verified callbacks by transactionId, requestId, and transactionStatus. Using only transactionId and status conflates separate refunds against the same original payment.
For APM-payment callbacks, parse JSON numbers without losing their source representation. The JavaScript example uses lossless-json and works on Node.js 20. Install it with npm install lossless-json.
const crypto = require('crypto');
const { isLosslessNumber, parse: parseLossless } = require('lossless-json');
function generateChecksum(fields, merchantSecret) {
const data = fields.join('|');
return crypto
.createHmac('sha256', merchantSecret)
.update(data)
.digest('base64');
}
function parseCallbackPayload(rawPayload) {
return parseLossless(rawPayload);
}
function checksumField(value) {
return isLosslessNumber(value) ? value.toString() : value;
}
// Request checksum
const requestChecksum = generateChecksum(
['merchant_001', '10.00', 'USD', 'req-789123'],
'your_merchant_secret'
);
function callbackFields(payload, callbackType) {
const fields = callbackType === 'apm-payment'
? [payload.accountId, payload.orderAmount, payload.orderCurrency, payload.transactionId]
: [payload.transactionId, payload.requestId, payload.transactionStatus];
return fields.map(checksumField);
}
function verifyCallbackChecksum(rawPayload, callbackType, receivedChecksum, merchantSecret) {
const payload = parseCallbackPayload(rawPayload);
const fields = callbackFields(payload, callbackType);
const computed = generateChecksum(fields, merchantSecret);
const computedBytes = Buffer.from(computed);
const receivedBytes = Buffer.from(receivedChecksum || '');
return computedBytes.length === receivedBytes.length
&& crypto.timingSafeEqual(computedBytes, receivedBytes);
}#Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Converting amount to minor units ("1000") | Checksum mismatch | Use the decimal string as-is: "10.00" |
| Wrong field order | Checksum mismatch | Use the exact order in the callback matrix |
| Hex or URL-safe Base64 output | Checksum mismatch | Use standard Base64, preserving +, /, and = |
| Using the APM-payment formula for APM payout | Checksum mismatch | APM payout uses the three-field transaction-result formula |
Using refundId as refund transactionId | Checksum mismatch | Use the original card-payment ID; signed requestId identifies the refund |
Reformatting APM-payment orderAmount | Checksum mismatch | Use the exact callback representation |
| String comparison (not constant-time) | Timing attack vulnerability | Use timingSafeEqual / hmac.compare_digest |
#Security Best Practices
- Never expose your
merchantSecretin client-side code or logs. - Always verify both request and callback checksums.
- Use constant-time comparison to prevent timing attacks.
- Process card refunds idempotently by
transactionId,requestId, andtransactionStatus; other callbacks usetransactionIdand status. - Rotate
merchantSecretperiodically for better security.