PAYWISE
← Semua artikel

Pembangun

Verifying PAYWISE webhook signatures

Every webhook is signed with HMAC-SHA256. Here is how to check the signature in PHP and Node.js, and how to handle retries safely.

Diterbitkan 03 Oktober 2026 2 min bacaan

Artikel ini belum tersedia dalam Bahasa Melayu. Ia dipaparkan dalam English.

Juga tersedia dalam: Indonesia, 简体中文

Verifying PAYWISE webhook signatures

When a payment is paid, fails or expires, PAYWISE sends a webhook to your server. Because your webhook URL is public, anyone could send a fake "paid" message to it. That is why every PAYWISE webhook is signed, and why your server should check the signature before doing anything.

How the signature works

Each delivery is a POST with a JSON body and an X-PAYWISE-Signature header. The header is the hex-encoded HMAC-SHA256 of the raw request body, using your webhook secret as the key. You find the secret on the Webhooks page of your dashboard.

Verifying a webhook: raw body plus secret, HMAC-SHA256, then compare with the X-PAYWISE-Signature header

Two details matter:

  1. Use the raw body, exactly as received. If you parse the JSON and encode it again, spaces or key order can change and the signature will not match.
  2. Compare with a constant-time function (hash_equals in PHP, crypto.timingSafeEqual in Node.js).

PHP

$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', $payload, getenv('PAYWISE_WEBHOOK_SECRET'));
$received = $_SERVER['HTTP_X_PAYWISE_SIGNATURE'] ?? '';

if (! hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}

$event = json_decode($payload, true);
if ($event['event'] === 'payment.paid') {
    // Mark $event['data']['merchant_reference'] as paid (once).
}
http_response_code(200);

Node.js (Express)

app.post('/webhooks/paywise', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', process.env.PAYWISE_WEBHOOK_SECRET)
    .update(req.body).digest('hex');
  const received = req.get('X-PAYWISE-Signature') || '';
  const ok = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  // handle event.event: payment.paid, payment.failed, payment.expired
  res.sendStatus(200);
});

The PHP and Node.js SDKs include a ready-made verify function.

Retries and duplicates

If your server does not answer with a 2xx status, PAYWISE retries the delivery automatically. Every attempt, with the status your server returned, is listed on your Webhooks page, and you can resend one from the transaction page. This means the same event can arrive more than once: make your handler idempotent, for example by ignoring a payment.paid for an order that is already paid.

Events you can receive

  • payment.paid, payment.failed, payment.expired for transactions,
  • withdrawal.completed, withdrawal.failed, withdrawal.rejected for your withdrawals.

Answer quickly with a 2xx status and do slow work (emails, stock updates) in the background.

Lagi artikel

Mulakan hari ini dengan kunci sandbox.

Buat akaun, jana kunci sandbox dan hantar transaksi pertama anda dari playground dalam beberapa minit.