Loyalty Webhooks
Webhook Event Signatures
Verify that webhook requests come from TrueLoyal.
Before you process a webhook request, verify that it was sent by TrueLoyal and not by a third party. Every request is signed with an HMAC (hash-based message authentication code) computed with the webhook's secret key, and includes two headers:
x-zinrelo-signature: the signature.nonce: the Unix time, in milliseconds, at which the request was signed.
To verify the signature:
- Build the message by joining the raw request body, exactly as received, a colon (
:) and thenonceheader. - Compute the HMAC-SHA512 of the message using the webhook's secret key, as a hexadecimal digest.
- Compare the result with the
x-zinrelo-signatureheader using a constant-time comparison.
The following examples show how to verify a signature in Ruby, Python, PHP and Java.
require "openssl"
# payload is the raw request body (e.g. request.raw_post in Rails); signature and nonce are the x-zinrelo-signature
# and nonce headers.
def valid_signature?(payload, signature, nonce, secret)
return false if signature.to_s.empty? || nonce.to_s.empty?
expected = OpenSSL::HMAC.hexdigest("SHA512", secret, "#{payload}:#{nonce}")
OpenSSL.secure_compare(expected, signature)
endimport hashlib
import hmac
# payload is the raw request body as bytes (e.g. request.get_data() in Flask, request.body in Django); signature and
# nonce are the x-zinrelo-signature and nonce headers.
def is_valid_signature(payload: bytes, signature: str, nonce: str, secret: str) -> bool:
if not signature or not nonce:
return False
message = payload + b":" + nonce.encode()
expected = hmac.new(secret.encode(), message, hashlib.sha512).hexdigest()
return hmac.compare_digest(expected, signature)<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ZINRELO_SIGNATURE'] ?? '';
$nonce = $_SERVER['HTTP_NONCE'] ?? '';
$secret = '<SECRET_KEY>';
$expected = hash_hmac('sha512', $payload . ':' . $nonce, $secret);
if ($signature === '' || $nonce === '' || !hash_equals($expected, $signature)) {
http_response_code(400);
exit;
}
// The signature is valid: process the events.import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class WebhookSignature {
// payload is the raw request body; signature and nonce are the x-zinrelo-signature and nonce headers.
public static boolean isValid(byte[] payload, String signature, String nonce, String secret) throws Exception {
if (signature == null || signature.isEmpty() || nonce == null || nonce.isEmpty()) {
return false;
}
Mac mac = Mac.getInstance("HmacSHA512");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA512"));
mac.update(payload);
mac.update((":" + nonce).getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(mac.doFinal());
return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8));
}
}