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 with sha256=.
  • X-XPayr-Event: event type such as payment.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.