Skip to main content
SDKs & Libraries

Webhook Signature Helpers

Copy-paste ready signature verification functions for any language. No SDK required — these are standalone functions you can drop directly into your application.

How XRNotify signatures work

  1. XRNotify computes HMAC-SHA256(raw_body, webhook_secret)
  2. The result is formatted as sha256=<hex_digest>
  3. This value is sent in the X-XRNotify-Signature request header
  4. You recompute the same HMAC on your side and compare using a constant-time function

Always use raw bytes: Compute the HMAC over the raw request body bytes before any JSON parsing. Modifying the body (whitespace changes, key reordering) will invalidate the signature.

Node.js

CommonJS
const crypto = require('crypto');

/**
 * Verify an XRNotify webhook signature.
 * @param {Buffer|string} payload - Raw request body (before JSON.parse)
 * @param {string} signature - X-XRNotify-Signature header value
 * @param {string} secret - Webhook secret
 * @returns {boolean}
 */
function verifyXRNotifySignature(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature, 'utf8'),
      Buffer.from(expected, 'utf8')
    );
  } catch {
    // Lengths differ — timingSafeEqual throws on mismatched lengths
    return false;
  }
}

Key point: crypto.timingSafeEqual throws if the two buffers have different lengths, so the try/catch is required. Pass the raw req.body Buffer — not a parsed object.

Python

stdlib only
import hmac
import hashlib


def verify_xrnotify_signature(payload: bytes, signature: str, secret: str) -> bool:
    """
    Verify an XRNotify webhook signature.

    payload:   raw request body bytes (before json.loads)
    signature: value of X-XRNotify-Signature header
    secret:    webhook secret string
    """
    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Key point: hmac.compare_digest performs a constant-time comparison, preventing timing-based attacks. Pass request.data (bytes) in Flask, or await request.body() in FastAPI.

Go

stdlib only
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

// VerifyXRNotifySignature verifies an XRNotify webhook signature.
// payload:   raw request body bytes (from io.ReadAll(r.Body))
// signature: X-XRNotify-Signature header value
// secret:    webhook secret
func VerifyXRNotifySignature(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := fmt.Sprintf("sha256=%s", hex.EncodeToString(mac.Sum(nil)))
    return hmac.Equal([]byte(signature), []byte(expected))
}

Key point: hmac.Equal from the standard library performs a constant-time byte comparison. Read the body with io.ReadAll(r.Body) and pass the bytes directly to this function.

Ruby

stdlib + rack
require 'openssl'
require 'rack'

# Verify an XRNotify webhook signature.
# payload:   raw request body string
# signature: X-XRNotify-Signature header value
# secret:    webhook secret
def verify_xrnotify_signature(payload, signature, secret)
  expected = 'sha256=' + OpenSSL::HMAC.hexdigest('sha256', secret, payload)
  Rack::Utils.secure_compare(signature, expected)
end

# Sinatra / Rails example:
# post '/webhooks/xrpl' do
#   raw = request.body.read
#   sig = request.env['HTTP_X_XRNOTIFY_SIGNATURE']
#   halt 401, 'Unauthorized' unless verify_xrnotify_signature(raw, sig, ENV['WEBHOOK_SECRET'])
#   event = JSON.parse(raw)
#   # handle event...
#   status 200
# end

Key point: Rack::Utils.secure_compare provides constant-time string comparison. Read the raw body with request.body.read before passing to JSON.parse.

PHP

stdlib only
<?php

/**
 * Verify an XRNotify webhook signature.
 *
 * @param string $payload   Raw request body
 * @param string $signature X-XRNotify-Signature header value
 * @param string $secret    Webhook secret
 * @return bool
 */
function verifyXRNotifySignature(string $payload, string $signature, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

// Usage example:
// $payload   = file_get_contents('php://input');
// $signature = $_SERVER['HTTP_X_XRNOTIFY_SIGNATURE'] ?? '';
// $secret    = getenv('WEBHOOK_SECRET');
//
// if (!verifyXRNotifySignature($payload, $signature, $secret)) {
//     http_response_code(401);
//     exit('Unauthorized');
// }
//
// $event = json_decode($payload, true);
// // handle $event...

Key point: hash_equals performs a constant-time string comparison (available since PHP 5.6). Read the raw body with file_get_contents('php://input') before decoding JSON.

Java

JDK only
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;

/**
 * Verify an XRNotify webhook signature.
 *
 * @param payload   raw request body bytes
 * @param signature X-XRNotify-Signature header value
 * @param secret    webhook secret string
 * @return true if the signature is valid
 */
public static boolean verifyXRNotifySignature(
    byte[] payload, String signature, String secret
) throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
    String expected = "sha256=" + bytesToHex(mac.doFinal(payload));

    // Use MessageDigest.isEqual for constant-time comparison
    return MessageDigest.isEqual(
        expected.getBytes("UTF-8"),
        signature.getBytes("UTF-8")
    );
}

private static String bytesToHex(byte[] bytes) {
    StringBuilder sb = new StringBuilder(bytes.length * 2);
    for (byte b : bytes) {
        sb.append(String.format("%02x", b));
    }
    return sb.toString();
}

Key point: MessageDigest.isEqual performs a constant-time byte array comparison (available since Java 6). Read the raw request body bytes with request.getInputStream().readAllBytes() in a servlet before parsing JSON.

SDK documentation