Appearance
Webhooks
Webhooks tell your server when a sale or collection reaches a final status, so you don't have to poll.
Set the URL in the portal: Developers → Webhooks. You get a whsec_ secret once; keep it on your server.
Events
| Event | When |
|---|---|
transaction.completed · transaction.failed · transaction.reversed | A sale's final status |
collection.completed · collection.failed · collection.reversed | A collection's final status |
partner.paused | A reversal took your balance below zero |
webhook.test | You pressed Send test event or called POST /webhooks/test |
The body is thin. GET the object for full details:
json
{"event_id":"evt_…","type":"transaction.completed","environment":"test","object_id":"tx_…","reference":"ORDER-1001","status":"completed","occurred_at":"2026-10-17T14:05:00+02:00"}Verify every request
Headers: X-Xash-Event-Id, X-Xash-Event-Type, X-Xash-Timestamp, X-Xash-Signature.
The signature is the hex HMAC-SHA256 of "{X-Xash-Timestamp}.{raw body}" with your secret. Use the raw body exactly as received, before parsing. Reject the request if the signature doesn't match or the timestamp is more than 5 minutes old.
php
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_XASH_TIMESTAMP'] ?? '';
$expected = hash_hmac('sha256', $ts.'.'.$raw, getenv('XASH_WEBHOOK_SECRET'));
if (! hash_equals($expected, $_SERVER['HTTP_X_XASH_SIGNATURE'] ?? '') || abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}js
import crypto from 'node:crypto';
// rawBody: the request body as a Buffer, before JSON parsing
const ts = req.get('X-Xash-Timestamp');
const expected = crypto.createHmac('sha256', process.env.XASH_WEBHOOK_SECRET).update(`${ts}.${rawBody}`).digest('hex');
const given = req.get('X-Xash-Signature') ?? '';
const ok = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))
&& Math.abs(Date.now() / 1000 - Number(ts)) <= 300;python
import hmac, hashlib, os, time
expected = hmac.new(os.environ["XASH_WEBHOOK_SECRET"].encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, signature) and abs(time.time() - int(ts)) <= 300Delivery
- Answer with any
2xxwithin 10 seconds. Do the slow work afterwards. - Failures are retried after 30 s, 1 min, 2 min… up to once an hour, for 72 hours.
- The same event can arrive more than once. Ignore
event_ids you've already handled. - The portal shows every delivery, with Redeliver.
- Webhook URLs must be public
https://addresses on port 443.