Verify Signature
Every webhook delivery includes three HTTP headers for signature verification:
Header | Description |
hmac | HMAC-SHA256 signature of the signing data |
apikey | The API key identifier used for signing |
timestamp | Unix epoch milliseconds at time of signing |
Verification Algorithm
- Extract the apikey, timestamp, and hmac headers from the request
- Read the raw request body (the JSON payload)
- Construct the signing data: {apikey}:{timestamp}:{body}
- Compute HMAC-SHA256 of the signing data using your API key's secret
- Compare your computed signature with the hmac header value
Sample Code
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public boolean verifyWebhook(String apiKey, String timestamp, String hmacHeader,
String body, String secret) {
String signingData = apiKey + ":" + timestamp + ":" + body;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
byte[] hash = mac.doFinal(signingData.getBytes(StandardCharsets.UTF_8));
String computed = bytesToHex(hash);
return computed.equalsIgnoreCase(hmacHeader);
}
Python
import hmac
import hashlib
def verify_webhook(api_key: str, timestamp: str, hmac_header: str,
body: str, secret: str) -> bool:
signing_data = f"{api_key}:{timestamp}:{body}"
computed = hmac.new(
secret.encode('utf-8'),
signing_data.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed.lower(), hmac_header.lower())
Node.js
const crypto = require('crypto');
function verifyWebhook(apiKey, timestamp, hmacHeader, body, secret) {
const signingData = `${apiKey}:${timestamp}:${body}`;
const computed = crypto
.createHmac('sha256', secret)
.update(signingData)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed, 'hex'),
Buffer.from(hmacHeader, 'hex')
);
} Replay Protection
To guard against replay attacks, verify that the timestamp header is within an acceptable window (e.g. 5 minutes) of your server's current time:
long receivedTimestamp = Long.parseLong(timestampHeader);
long now = System.currentTimeMillis();
if (Math.abs(now - receivedTimestamp) > 300_000) { // 5 minutes
// Reject — potential replay attack
} Retry Behaviour
If your endpoint fails to respond with a 2xx status within 15 seconds, the platform retries delivery with exponential backoff:
Attempt | Delay After Failure |
1st retry | 1 minute |
2nd retry | 5 minutes |
3rd retry | 30 minutes |
4th retry | 2 hours |
5th retry | 8 hours |
6th retry | 24 hours |
After all 6 retries are exhausted, the delivery is marked as permanently failed.
Automatic Endpoint Disabling
If 5 consecutive events all permanently fail delivery (all retries exhausted for each), the endpoint is automatically disabled. You will need to re-enable it manually in the Merchant Portal after resolving the issue.
When you re-enable a disabled endpoint, delivery resumes for new events only — events that occurred while the endpoint was disabled are not retroactively delivered.
On this page
- Verify Signature