Skip to content
UnknownPay
ไทย
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

HeaderValue
Content-Typeapplication/json
X-Webhook-SignatureHMAC-SHA256 of the raw request body bytes, signed with your signing secret, encoded as hex (lowercase).
X-Webhook-Event-IdThe 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);
});