Skip to main content

webhook events & security

because payment transfers on the Faster Payment System (FPS) occur asynchronously, you must not rely on the client-side redirect to fulfill orders. instead, your server should listen for asynchronous webhook event callbacks dispatched by divit.

our gateway sends secure POST requests to your configured webhookEvents URL whenever the status of a payment order is updated.


event status catalog

divit dispatches a webhook for the following state transitions:

Event IDTitleDescriptionRecommended Action
2001payment successthe customer has successfully completed the bank transfer, and the funds are secured.fulfill the order: mark the order as paid, release goods/services, and notify the customer.
4001payment expiredthe order's expiration window (expiredAt) has passed, and the transaction has been closed without settlement.release inventory: close the checkout session and return any reserved items back to your catalog.

event payload structure

webhook payloads are delivered as an unauthenticated POST request containing a standardized JSON payload structure:

{
"event": {
"eventId": 2001,
"eventDescription": "Order payment completed successfully"
},
"eventData": {
"OrderID": "87418689-8f26-4200-8d6e-8c4430b41759",
"OrderAmount": {
"amount": 12050,
"currency": "HKD"
},
"MerchantRef": "ORDER-10024A"
}
}

properties reference

  • event.eventId (integer): the specific event status code (2001 for success, 4001 for expired).
  • eventData.OrderID (string): divit's unique identifier for the payment order. use this UUID to lookup the corresponding order in your database.
  • eventData.OrderAmount (object): structured object containing the transaction value (amount in cents/smallest subunit, and currency).
  • eventData.MerchantRef (string): the custom reference string you supplied when initiating the payment order.

cryptographic signature verification (HMAC-SHA256)

to prevent spoofing and fraudulent notifications, you must cryptographically validate every incoming webhook request's signature before executing any state changes or order fulfillment.

every legitimate webhook request from divit includes a custom signature header:

X-DIVIT-SIGNATURE: t=1683611281,s1=7ceJBJKe3TpZY55u3NxQldIDtL7LdcujDPFM7B53Bgw=

signature parameters

  • t: unix timestamp (seconds) of when the webhook was generated by our gateway. use this to prevent replay attacks by checking if the timestamp is recent (e.g., within 5 minutes).
  • s1: the Base64 encoded HMAC-SHA256 signature, generated by hashing the payload content.

signature verification algorithm

to verify the payload signature, reconstruct the expected signature locally and compare it to the s1 signature parameter:

  1. extract parameters: parse the X-DIVIT-SIGNATURE header. split the header by commas (,), then split by equals (=) to extract the values for t and s1.
  2. reconstruct content string: concatenate the timestamp t, a literal period character (.), and the raw, unparsed JSON request body. signatureContent = t + "." + rawRequestBody
  3. generate local hash: compute an HMAC-SHA256 signature of the signatureContent using your local secret DIVIT_SIGNATURE_KEY as the cryptographic key.
  4. encode and compare: convert the computed hash to a Base64-encoded string. perform a constant-time string comparison to compare your computed string against the extracted s1 signature. if they match exactly, the payload is verified.

reusable verification implementations

select an implementation template below for your backend stack:

const crypto = require('crypto');

/**
* Verifies a cryptographically-signed Divit Webhook payload
* @param {string} headerVal - The X-DIVIT-SIGNATURE header value
* @param {string} rawBody - The raw, unparsed request body string
* @param {string} signatureKey - Your local merchant webhook signature key (secret)
* @returns {boolean} - True if signature is valid
*/
function verifyDivitSignature(headerVal, rawBody, signatureKey) {
if (!headerVal || !rawBody || !signatureKey) {
return false;
}

// Extract t and s1 parameters
const params = {};
headerVal.split(',').forEach(item => {
const [key, value] = item.trim().split('=');
params[key] = value;
});

const timestamp = params['t'];
const expectedSignature = params['s1'];

if (!timestamp || !expectedSignature) {
return false;
}

// Recreate the signature content: t.rawBody
const signatureContent = `${timestamp}.${rawBody}`;

// Compute the HMAC SHA256 signature
const calculatedSignature = crypto
.createHmac('sha256', signatureKey)
.update(signatureContent)
.digest('base64');

// Secure constant-time comparison
try {
return crypto.timingSafeEqual(
Buffer.from(calculatedSignature),
Buffer.from(expectedSignature)
);
} catch (e) {
return calculatedSignature === expectedSignature;
}
}