> ## Documentation Index
> Fetch the complete documentation index at: https://beta-developers.mizaniyapay.dz/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify VTPE Webhook Signatures with HMAC-SHA256

> Learn how to verify VTPE webhook X-Signature headers using HMAC-SHA256 over the raw payload concatenated with the X-Timestamp value.

Mizaniya Pay VTPE sends webhooks with an `X-Signature` header so you can verify that each event truly came from VTPE. This page explains how the signature is computed and how to validate it in your server code.

## How the Signature Is Computed

VTPE signs every webhook using HMAC-SHA256 with your HMAC Secret. The message input is the raw request body concatenated directly with the `X-Timestamp` header value (no delimiter or separator). The resulting digest is plain hex and placed in the `X-Signature` header with no prefix.

```text theme={null}
Signature = HMAC-SHA256(rawBody + timestamp, HMAC_SECRET)
Header:    X-Signature: <hex digest>
```

For example, if the raw body is `{"event":"payment.success","data":{"reference":"ORD-123"}}` and `X-Timestamp` is `1718000000`, the signed message is exactly:

```text theme={null}
{"event":"payment.success","data":{"reference":"ORD-123"}}1718000000
```

## Verification Steps

Use raw request bytes (before any JSON parsing) when computing the signature. Polling or re-serializing the payload will change whitespace and break verification.

<Steps>
  <Step title="Read the raw body before JSON parsing">
    Capture the exact bytes VTPE sent. In Express, use `express.raw()`. In Flask, use `request.get_data()`.
  </Step>

  <Step title="Read the X-Timestamp header">
    Capture the exact `X-Timestamp` string value from the incoming request headers.
  </Step>

  <Step title="Read the X-Signature header">
    Capture the exact `X-Signature` hex string sent by VTPE.
  </Step>

  <Step title="Compute the expected signature">
    Calculate `HMAC-SHA256(rawBody + timestamp, HMAC_SECRET)` and encode the result as a lowercase hex string.
  </Step>

  <Step title="Compare signatures in constant time">
    Use a timing-safe comparison function (for example, `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python).
  </Step>

  <Step title="Return HTTP 200 on success, 400 on failure">
    If the signatures match, parse the body and respond with `200` and `{"success": true}`. If they do not match, respond with `400`.
  </Step>
</Steps>

## Code Examples

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "crypto";
  import express from "express";

  const app = express();

  // Use raw body middleware BEFORE JSON parsing
  app.use("/webhooks/vtpe", express.raw({ type: "application/json" }));

  app.post("/webhooks/vtpe", (req, res) => {
    const rawBody = req.body.toString("utf8");
    const timestamp = req.headers["x-timestamp"] ?? "";
    const receivedSig = req.headers["x-signature"] ?? "";

    const expectedSig = crypto
      .createHmac("sha256", process.env.VTPE_HMAC_SECRET)
      .update(rawBody + timestamp)
      .digest("hex");

    const valid = crypto.timingSafeEqual(
      Buffer.from(expectedSig, "utf8"),
      Buffer.from(receivedSig, "utf8")
    );

    if (!valid) {
      return res.status(400).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(rawBody);
    // handle event...
    return res.status(200).json({ success: true });
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import os
  from flask import Flask, request, jsonify

  app = Flask(__name__)

  @app.route("/webhooks/vtpe", methods=["POST"])
  def handle_webhook():
      raw_body = request.get_data(as_text=True)
      timestamp = request.headers.get("X-Timestamp", "")
      received_sig = request.headers.get("X-Signature", "")

      expected_sig = hmac.new(
          os.environ["VTPE_HMAC_SECRET"].encode(),
          (raw_body + timestamp).encode(),
          hashlib.sha256
      ).hexdigest()

      if not hmac.compare_digest(expected_sig, received_sig):
          return jsonify({"error": "Invalid signature"}), 400

      event = request.get_json(force=True)
      # handle event...
      return jsonify({"success": True}), 200
  ```
</CodeGroup>

<Warning>
  Always read the raw body before JSON parsing or body re-serialization. Re-encoding a parsed object may change field order or whitespace and cause the computed signature to differ from `X-Signature`, even for otherwise valid requests.
</Warning>

<Tip>
  After verifying the signature, also validate that `X-Timestamp` is within plus or minus five minutes of your server's current time. Rejecting old timestamps prevents replay attacks where an attacker resends a captured webhook request.
</Tip>
