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:
payment.succeeded— a checkout completed, whether or not the buyer ever returned to your site.payment.refunded— a refund was issued, from your own refund API call or from the Queye Pay dashboard directly.payment.disputed— the buyer's bank opened a chargeback.
{
"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.