> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rmz.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Signature Verification

> Verify the authenticity of RMZ webhook deliveries using HMAC-SHA256 signatures.

Every RMZ webhook delivery is signed with HMAC-SHA256. Verifying the signature ensures the request genuinely came from RMZ and was not tampered with in transit.

<Tip>
  Signature verification is the **only** reliable way to confirm a webhook is from RMZ. Anyone who learns your webhook URL can POST to it — without verification, they could grant themselves paid access (e.g., on a SaaS paywall) or trigger fulfillment on fake orders.
</Tip>

## How signing works

1. RMZ serializes the webhook payload to JSON
2. The JSON string is signed using HMAC-SHA256 with your webhook's secret key
3. The resulting hex-encoded hash is sent in the `Signature` header
4. Your server recomputes the hash and compares it to the header

```http theme={null}
POST /your-endpoint HTTP/1.1
Content-Type: application/json
Signature: a1b2c3d4e5f6...
X-RMZ-REQUEST-ID: 12345
X-RMZ-WEBHOOKS: 1.2

{"event":"order.created","data":{...}}
```

## Where to find your secret key

In the dashboard:

1. Go to **الإعدادات** → **الويب هوك (Webhooks)**
2. Click **معاينة** on the webhook you want to verify
3. The **المفتاح السري للتوقيع (HMAC Signing Key)** field shows the secret. Click the eye icon to reveal it, the copy icon to copy.

If you ever suspect the secret has leaked, click the **regenerate** (↻) button next to the key. The old key stops working immediately — make sure your server-side verification is updated **before** the next webhook fires.

## Verification steps

<Steps>
  <Step title="Extract the signature">
    Read the `Signature` header from the incoming request.
  </Step>

  <Step title="Get the raw request body">
    Read the raw body as a string. **Do not parse to JSON first** — the signature is computed on the exact bytes RMZ sent. Re-serializing changes whitespace and key ordering.
  </Step>

  <Step title="Compute the expected signature">
    Calculate `HMAC-SHA256(raw_body, secret_key)` and hex-encode the result.
  </Step>

  <Step title="Compare in constant time">
    Use a constant-time comparison (`hash_equals` in PHP, `hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node) to avoid timing attacks. Reject the request with HTTP 401 on mismatch.
  </Step>
</Steps>

## Code examples

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const crypto = require("crypto");
  const express = require("express");

  const app = express();

  function verifySignature(rawBody, signature, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(rawBody)
      .digest("hex");

    // Both buffers must be the same length for timingSafeEqual.
    if (signature.length !== expected.length) return false;
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  }

  // IMPORTANT: use express.raw, not express.json — verification needs raw bytes.
  app.post(
    "/webhooks/rmz",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const signature = req.headers["signature"];
      const secret = process.env.RMZ_WEBHOOK_SECRET;

      if (!verifySignature(req.body.toString(), signature, secret)) {
        return res.status(401).json({ error: "Invalid signature" });
      }

      const payload = JSON.parse(req.body);
      // Safe to process — handle payload.event and payload.data here.
      res.status(200).json({ received: true });
    }
  );
  ```

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

  app = Flask(__name__)

  def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode("utf-8"),
          raw_body,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)

  @app.route("/webhooks/rmz", methods=["POST"])
  def handle_webhook():
      signature = request.headers.get("Signature", "")
      secret = "your_webhook_secret_key"

      if not verify_signature(request.data, signature, secret):
          return jsonify({"error": "Invalid signature"}), 401

      payload = request.get_json()
      # Safe to process — handle payload["event"] and payload["data"] here.
      return jsonify({"received": True}), 200
  ```

  ```php PHP theme={null}
  <?php

  function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool
  {
      $expected = hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $signature);
  }

  // Pure PHP webhook handler (vanilla / no framework)
  $signature = $_SERVER['HTTP_SIGNATURE'] ?? '';
  $rawBody   = file_get_contents('php://input');
  $secret    = getenv('RMZ_WEBHOOK_SECRET');

  if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
      http_response_code(401);
      echo json_encode(['error' => 'Invalid signature']);
      exit;
  }

  $payload = json_decode($rawBody, true);
  // Safe to process — handle $payload['event'] and $payload['data'] here.

  http_response_code(200);
  echo json_encode(['received' => true]);
  ```

  ```php Laravel theme={null}
  use Illuminate\Http\Request;
  use Illuminate\Support\Facades\Route;

  function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool
  {
      $expected = hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $signature);
  }

  Route::post('/webhooks/rmz', function (Request $request) {
      $signature = $request->header('Signature');
      $secret    = config('services.rmz.webhook_secret');
      $rawBody   = $request->getContent();

      if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
          abort(401, 'Invalid signature');
      }

      $payload = json_decode($rawBody, true);
      // Safe to process — handle $payload['event'] and $payload['data'] here.
      return response()->json(['received' => true]);
  });
  ```

  ```rust Rust (Axum) theme={null}
  // Cargo.toml dependencies:
  //   axum = "0.7"
  //   hmac = "0.12"
  //   sha2 = "0.10"
  //   hex = "0.4"
  //   subtle = "2.5"
  //   tokio = { version = "1", features = ["full"] }

  use axum::{
      body::Bytes,
      extract::State,
      http::{HeaderMap, StatusCode},
      routing::post,
      Json, Router,
  };
  use hmac::{Hmac, Mac};
  use sha2::Sha256;
  use subtle::ConstantTimeEq;

  type HmacSha256 = Hmac<Sha256>;

  #[derive(Clone)]
  struct AppState {
      webhook_secret: String,
  }

  fn verify_signature(body: &[u8], signature_hex: &str, secret: &str) -> bool {
      let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) {
          Ok(m) => m,
          Err(_) => return false,
      };
      mac.update(body);
      let expected = hex::encode(mac.finalize().into_bytes());

      // Constant-time comparison to prevent timing attacks.
      expected.as_bytes().ct_eq(signature_hex.as_bytes()).into()
  }

  async fn webhook_handler(
      State(state): State<AppState>,
      headers: HeaderMap,
      body: Bytes,
  ) -> Result<Json<serde_json::Value>, StatusCode> {
      let signature = headers
          .get("Signature")
          .and_then(|h| h.to_str().ok())
          .unwrap_or("");

      if !verify_signature(&body, signature, &state.webhook_secret) {
          return Err(StatusCode::UNAUTHORIZED);
      }

      let payload: serde_json::Value =
          serde_json::from_slice(&body).map_err(|_| StatusCode::BAD_REQUEST)?;

      // Safe to process — handle payload["event"] and payload["data"] here.
      let _ = payload;

      Ok(Json(serde_json::json!({ "received": true })))
  }

  #[tokio::main]
  async fn main() {
      let state = AppState {
          webhook_secret: std::env::var("RMZ_WEBHOOK_SECRET")
              .expect("RMZ_WEBHOOK_SECRET not set"),
      };

      let app = Router::new()
          .route("/webhooks/rmz", post(webhook_handler))
          .with_state(state);

      let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
      axum::serve(listener, app).await.unwrap();
  }
  ```

  ```go Go (net/http) theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "io"
      "net/http"
      "os"
  )

  func verifySignature(body []byte, signature, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(body)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(signature), []byte(expected))
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
      body, err := io.ReadAll(r.Body)
      if err != nil {
          http.Error(w, "bad body", http.StatusBadRequest)
          return
      }

      signature := r.Header.Get("Signature")
      secret := os.Getenv("RMZ_WEBHOOK_SECRET")

      if !verifySignature(body, signature, secret) {
          http.Error(w, "invalid signature", http.StatusUnauthorized)
          return
      }

      // Safe to process — parse body and handle the event.
      w.WriteHeader(http.StatusOK)
  }
  ```
</CodeGroup>

## Common pitfalls

<AccordionGroup>
  <Accordion title="Why use the raw body, not parsed JSON?">
    The signature is computed on the exact JSON string RMZ sends. If you parse and re-serialize it, whitespace and key ordering can change — producing a different hash and a false mismatch. Always verify against the raw request body.
  </Accordion>

  <Accordion title="My signature always fails — what's wrong?">
    Most common causes:

    * You're computing the hash on parsed JSON instead of the raw body
    * Your framework strips trailing whitespace or BOM from the request body
    * You copied the secret with leading/trailing whitespace
    * You're using the wrong webhook's secret (each webhook has its own key)
    * The webhook was rotated and you haven't updated your server with the new key

    Check that `Content-Length` matches the actual body length, and log both the expected and received hashes during debugging (then remove the logs).
  </Accordion>

  <Accordion title="How do I rotate the secret key?">
    In the dashboard:

    1. Open the webhook in **معاينة** mode
    2. Click the **regenerate** (↻) button next to the key
    3. Confirm — the old key stops working immediately
    4. Update your server's `RMZ_WEBHOOK_SECRET` environment variable with the new key
    5. Redeploy

    Plan a brief outage window when rotating, since any in-flight webhook delivered between the regeneration and your redeploy will fail verification.
  </Accordion>

  <Accordion title="Are retries signed too?">
    Yes. Every retry uses the **current** webhook secret. If you rotate the secret while a webhook is being retried, the next retry will be signed with the new key.
  </Accordion>

  <Accordion title="What about the X-RMZ-REQUEST-ID header?">
    Use it for **idempotency** in your handler — if you receive two requests with the same `X-RMZ-REQUEST-ID`, treat them as the same event. RMZ may retry on transient failures (5xx, network errors) up to the configured `tries` count.
  </Accordion>
</AccordionGroup>

<Tip>
  Even with signature verification, validate the payload shape before acting on it: check that `event` is one you handle and that `data` contains the expected fields. Defensive parsing prevents bugs from breaking-but-still-signed payloads (e.g., during RMZ rolling deploys).
</Tip>
