Home
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:

  1. Build the message by joining the raw request body, exactly as received, a colon (:) and the nonce header.
  2. Compute the HMAC-SHA512 of the message using the webhook's secret key, as a hexadecimal digest.
  3. Compare the result with the x-zinrelo-signature header 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)
end
import 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));
    }
}