Webhooks
Signature Verification
Verify the X-Webhook-Signature HMAC-SHA256 on the raw request bytes before parsing JSON, with Node.js and Python examples
Every request we send carries these HTTP headers.
Webhook headers
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC-SHA256 of the raw request body bytes, signed with your signing secret, encoded as hex (lowercase). |
X-Webhook-Event-Id | The same value as event_id in the body (e.g. dep_abc123:deposit.success). |
The algorithm
HMAC-SHA256(secret, raw_body_bytes) → hex
secret= the signing secret issued during configuration (show-once in the Portal).
You must sign/verify over the RAW bytes of the body you received. Do not re-serialize the JSON before verifying — a different key order or whitespace will make the signature mismatch. Verify on the raw bytes before JSON parse, and compare in constant time.
Verify on the merchant side
const crypto = require('crypto');
// Must read the raw body before parsing JSON
app.use('/webhooks/unknownpay', express.raw({ type: 'application/json' }));
app.post('/webhooks/unknownpay', (req, res) => {
const secret = process.env.UNKNOWNPAY_WEBHOOK_SECRET;
const signature = req.header('X-Webhook-Signature') || '';
const expected = crypto.createHmac('sha256', secret)
.update(req.body) // req.body = Buffer of raw bytes
.digest('hex');
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString('utf8'));
// ... idempotency on event.event_id, then process ...
res.sendStatus(200);
});
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/unknownpay")
def webhook():
secret = os.environ["UNKNOWNPAY_WEBHOOK_SECRET"].encode()
raw = request.get_data() # raw bytes — do not use request.json
expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()
got = request.headers.get("X-Webhook-Signature", "")
if not hmac.compare_digest(got, expected):
abort(401)
event = request.get_json()
# ... idempotency on event["event_id"], then process ...
return "", 200
Event Catalog & Payloads
Every webhook event UnknownPay actually sends, plus the shared payload fields and real JSON examples for deposits and withdrawals
Endpoint Requirements, Retries & SSRF
What your webhook endpoint must do, the retry/backoff schedule, replay, and the HTTPS/SSRF rules that reject an INVALID_URL
