Skip to content
API DocsDocs

Checksum Authentication Guide

Generate and verify HMAC-SHA256 checksums for API requests and callbacks

8 min readUpdated Aug 3, 2026

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

DirectionLocationFields used, in order
APM request to ExiromBody field (checksum)accountIdamountcurrencyrequestId
Callback from ExiromHTTP header (X-Checksum)See the callback matrix below

#Callback Checksum Matrix

CallbackFields, in order
APM paymentaccountIdorderAmountorderCurrencytransactionId
Card payment, card payout, APM payouttransactionIdrequestIdtransactionStatus
Card refundtransactionId (original card-payment ID) → requestIdtransactionStatus

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

CurrencyRequest amountamount 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: amount is sent as a string (e.g. "200.00")
  • APM-payment callbacks: orderAmount is 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.0

When validating an APM-payment callback checksum, always use the exact orderAmount representation 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

MistakeSymptomFix
Converting amount to minor units ("1000")Checksum mismatchUse the decimal string as-is: "10.00"
Wrong field orderChecksum mismatchUse the exact order in the callback matrix
Hex or URL-safe Base64 outputChecksum mismatchUse standard Base64, preserving +, /, and =
Using the APM-payment formula for APM payoutChecksum mismatchAPM payout uses the three-field transaction-result formula
Using refundId as refund transactionIdChecksum mismatchUse the original card-payment ID; signed requestId identifies the refund
Reformatting APM-payment orderAmountChecksum mismatchUse the exact callback representation
String comparison (not constant-time)Timing attack vulnerabilityUse timingSafeEqual / hmac.compare_digest

#Security Best Practices

  • Never expose your merchantSecret in 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, and transactionStatus; other callbacks use transactionId and status.
  • Rotate merchantSecret periodically for better security.
Was this helpful?