Verify RizPay webhooks and process events safely

Verify the timestamp and raw-body HMAC signature on RizPay webhooks, then deduplicate events and reconcile purchases without exposing customer data.

Already registered? Open API settings and follow the business-access steps. Sandbox purchases do not deliver real services.

Reviewed

A webhook URL is reachable by other people. Before changing an order or creating a financial entry, verify that the message was signed with your endpoint's secret and that the signed request is recent.

Verify the bytes that were sent

RizPay signs the timestamp, a literal period and the raw request body with HMAC-SHA256. The signature header is X-RizPay-Signature in sha256=HEX format; the timestamp is in X-RizPay-Timestamp. Use the endpoint's webhook signing secret, not its API key.

Capture the raw body before a framework parses or reserializes JSON. Whitespace changes can invalidate the signature. Reject malformed timestamps and signatures, including digests with the wrong length, before comparing them.

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyRizPayWebhook(
  rawBody,
  timestamp,
  signature,
  secret,
  now = Date.now()
) {
  if (
    !Buffer.isBuffer(rawBody) ||
    typeof secret !== "string" ||
    !secret.startsWith("whsec_")
  )
    return false;
  if (typeof timestamp !== "string" || !/^\d{10}$/.test(timestamp))
    return false;
  if (
    typeof signature !== "string" ||
    !/^sha256=[a-fA-F0-9]{64}$/.test(signature)
  )
    return false;
  if (Math.abs(Math.floor(now / 1000) - Number(timestamp)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature.slice(7), "hex");
  return timingSafeEqual(expected, received);
}

A signature validates the request bytes. The timestamp check limits replay time, but it does not replace persistent deduplication.

Persist the event before acknowledging it

After verification, parse the JSON and validate the expected event shape. Use the signed body's event id as the deduplication key, scoped to the endpoint or partner context in your application. Put a unique constraint on that key. Persist a durable event or enqueue work durably before returning a successful acknowledgement.

Webhook transaction fields live under data.object, not data.attributes. In this envelope, reference is your external reference and rizpay_reference is RizPay's generated reference. The response to a REST purchase uses a different shape. Keep those parsers separate.

Reconcile effects once

Process the stored event under a transaction or order lock. Match it to the intended order, confirm the expected product and amount where applicable, and prevent an older event from rolling back newer state. If the state is uncertain, retrieve the transaction through the API.

A repeated event must not produce a second refund, receipt-triggered purchase or wallet entry. A legitimate later reversal is a different state change and must be handled explicitly.

Keep receipts private

The event can contain phone numbers, meter numbers and electricity tokens. Do not send the raw payload to browser analytics or public error messages. Log only the operational fields your team needs, under appropriate access controls.

Before launch, test a valid signature, altered body, missing headers, invalid digest length, stale timestamp and a valid duplicate event. Also test a temporary receiver outage and confirm that recovery does not duplicate financial effects.