PAYWISE
← Tất cả bài viết

Nhà phát triển

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.

Đăng ngày 03 tháng 10 2026 2 phút đọc

Bài viết này chưa có bản Tiếng Việt. Đang hiển thị bằng English.

Cũng có bằng: 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.

Bài viết khác

Bắt đầu ngay hôm nay với khóa sandbox.

Tạo tài khoản, tạo khóa sandbox và gửi giao dịch đầu tiên từ playground chỉ trong vài phút.