Webhook trust
Verify the event before you update the order.
XPayr signs webhook payloads with your webhook secret. Store the raw request body, verify X-XPayr-Signature, then process the event idempotently.
Headers to read
X-XPayr-Signature: HMAC SHA-256 signature prefixed withsha256=.X-XPayr-Event: event type such aspayment.completed.X-XPayr-Delivery: delivery/event identifier where available.
Node.js verification
import crypto from "node:crypto";
export function verifyXPayrWebhook(rawBody, signatureHeader, secret) {
const received = String(signatureHeader || "").replace(/^sha256=/, "");
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(received, "hex"),
Buffer.from(expected, "hex")
);
}
PHP verification
function verify_xpayr_webhook(string $rawBody, string $signatureHeader, string $secret): bool
{
$received = preg_replace('/^sha256=/', '', trim($signatureHeader));
$expected = hash_hmac('sha256', $rawBody, $secret);
return is_string($received) && hash_equals($expected, $received);
}
Python verification
import hmac
import hashlib
def verify_xpayr_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
received = signature_header.replace("sha256=", "").strip()
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received)
Go (Golang) verification
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"strings"
)
func VerifyXPayrWebhook(rawBody []byte, signatureHeader string, secret string) bool {
received := strings.TrimPrefix(strings.TrimSpace(signatureHeader), "sha256=")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}
Events to handle
At minimum, handle payment.completed, payment.failed, payment.expired, test.webhook. Use the session ID and your own metadata order ID to reconcile.