If a customer pays and closes the tab before returning to your store, your return URL never gets hit — the order would sit unconfirmed forever without another way to learn the payment went through. A webhook is that other way: set an HTTPS endpoint in your Queye Pay dashboard's Settings, under Vendor & products, and Queye Pay calls it directly, server to server.

Three events are sent, each as a POST with a JSON body:

{
  "event": "payment.succeeded",
  "created": "2026-09-23T02:10:00+00:00",
  "vendor": "your_vendor_key",
  "data": {
    "tr_id": "ABCD12345678",
    "status": "successful",
    "product": "Test Product",
    "amount": 2.99,
    "currency": "USD",
    "last4": "4242"
  }
}

Every request carries an x-queye-signature header shaped like t=<unix timestamp>,v1=<hex signature>. Verify it before trusting the body: recompute the HMAC-SHA256 of "{timestamp}.{raw body}" using your vendor secret as the key, and compare it to the signature using a constant-time comparison. Reject anything where the timestamp is more than a few minutes old, to block replay of a captured request.

$body = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_QUEYE_SIGNATURE"] ?? "";

// "t=1695430200,v1=abcdef..."
parse_str(str_replace(",", "&", $header), $parts);
$timestamp = (int)($parts["t"] ?? 0);
$given     = (string)($parts["v1"] ?? "");

$expected = hash_hmac("sha256", $timestamp . "." . $body, "your_secret_key");

if (abs(time() - $timestamp) > 300 || !hash_equals($expected, $given)) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
const crypto = require("crypto");

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const timestamp = parseInt(parts.t, 10);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp = int(parts.get("t", 0))
    expected = hmac.new(
        secret.encode(), f"{timestamp}.{raw_body.decode()}".encode(), hashlib.sha256
    ).hexdigest()

    if abs(time.time() - timestamp) > 300:
        return False
    return hmac.compare_digest(expected, parts.get("v1", ""))

Respond with a 2xx quickly — a webhook is retried with backoff if your endpoint doesn't acknowledge it, so a slow or failing endpoint gets called again rather than skipped.