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

# Webhook Signature Validation

> How to make sure the webhooks you receive are authentic and unmodified

## Purpose

Every webhook request sent by NoxPay is signed. Validating that signature guarantees the webhook:

* **Has not been tampered with** in transit (integrity)
* **Was actually sent by NoxPay** (authenticity)

<Warning>
  **Always** validate the signature **before** processing the webhook payload. Never trust an unverified payload.
</Warning>

## How it works

NoxPay signs each webhook using a shared secret (the **Webhook Secret**). On your side, you reproduce that same signature locally and compare it against the value received in the request header. If they match, the webhook is legitimate.

### Webhook Secret

* A secret key unique to your integration, generated by NoxPay
* Shared with you securely
* **Never** sent inside the webhook itself — only you and NoxPay know it

<Warning>
  Never expose your `webhook_secret` and never reuse your API Key as the secret. Store it securely (environment variable, secrets vault, etc.).
</Warning>

### Signature headers

Two signature methods are in use. Each arrives in a different header:

| Method       | Header            | Status                |
| ------------ | ----------------- | --------------------- |
| HMAC-SHA256  | `X-Signature`     | Current / recommended |
| Plain SHA256 | `X-Nox-Signature` | Legacy                |

<Note>
  Both headers may show up on integrations — including new ones. Your implementation should be prepared to validate both. Prioritize `X-Signature` (HMAC) and treat `X-Nox-Signature` as compatibility with the older method.
</Note>

***

## Current method — HMAC-SHA256

Header to compare: `X-Signature`

### Formula

```text theme={null}
Base64( HMAC_SHA256( key = webhook_secret, message = raw_body ) )
```

Where:

* `webhook_secret` → the shared secret
* `raw_body` → the request body **exactly as received** (raw bytes)
* encoding → UTF-8

### Step by step

<Steps>
  <Step title="Capture the raw body">
    Take the request body exactly as it arrived, **without parsing or re-serializing** the JSON.
  </Step>

  <Step title="Generate the HMAC-SHA256">
    Compute the HMAC using `webhook_secret` as the key and `raw_body` as the message.
  </Step>

  <Step title="Encode as Base64">
    Convert the binary HMAC output to a Base64 string.
  </Step>

  <Step title="Compare securely">
    Compare against the `X-Signature` header using a constant-time comparison function (to avoid *timing attacks*).
  </Step>
</Steps>

### Code examples

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib
  import base64

  def validate_hmac_signature(raw_body: bytes, webhook_secret: str, received_signature: str) -> bool:
      h = hmac.new(webhook_secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
      expected = base64.b64encode(h).decode()
      # Constant-time comparison to avoid timing attacks
      return hmac.compare_digest(expected, received_signature)
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function validateHmacSignature(rawBody, webhookSecret, receivedSignature) {
    const expected = crypto
      .createHmac("sha256", webhookSecret)
      .update(rawBody, "utf8")
      .digest("base64");

    // Constant-time comparison to avoid timing attacks
    const a = Buffer.from(expected);
    const b = Buffer.from(receivedSignature);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

  ```php PHP theme={null}
  <?php
  function validateHmacSignature(string $rawBody, string $webhookSecret, string $receivedSignature): bool {
      // hash_hmac with the 4th argument "true" returns raw bytes
      $expected = base64_encode(hash_hmac('sha256', $rawBody, $webhookSecret, true));
      // hash_equals performs a constant-time comparison
      return hash_equals($expected, $receivedSignature);
  }
  ```

  ```ruby Ruby theme={null}
  require 'openssl'
  require 'base64'

  def validate_hmac_signature(raw_body, webhook_secret, received_signature)
    digest   = OpenSSL::HMAC.digest('sha256', webhook_secret, raw_body)
    expected = Base64.strict_encode64(digest)
    # Constant-time comparison to avoid timing attacks
    OpenSSL.secure_compare(expected, received_signature)
  end
  ```

  ```go Go theme={null}
  package webhook

  import (
      "crypto/hmac"
      "crypto/sha256"
      "crypto/subtle"
      "encoding/base64"
  )

  func ValidateHMACSignature(rawBody, webhookSecret, receivedSignature string) bool {
      mac := hmac.New(sha256.New, []byte(webhookSecret))
      mac.Write([]byte(rawBody))
      expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))
      // Constant-time comparison to avoid timing attacks
      return subtle.ConstantTimeCompare([]byte(expected), []byte(receivedSignature)) == 1
  }
  ```

  ```java Java theme={null}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.util.Base64;

  public class WebhookValidator {
      public static boolean validateHmacSignature(String rawBody, String webhookSecret, String receivedSignature) throws Exception {
          Mac mac = Mac.getInstance("HmacSHA256");
          mac.init(new SecretKeySpec(webhookSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
          byte[] hash = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
          String expected = Base64.getEncoder().encodeToString(hash);
          // Constant-time comparison to avoid timing attacks
          return MessageDigest.isEqual(
              expected.getBytes(StandardCharsets.UTF_8),
              receivedSignature.getBytes(StandardCharsets.UTF_8));
      }
  }
  ```

  ```csharp C# theme={null}
  using System;
  using System.Security.Cryptography;
  using System.Text;

  public static class WebhookValidator
  {
      public static bool ValidateHmacSignature(string rawBody, string webhookSecret, string receivedSignature)
      {
          using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret));
          byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
          string expected = Convert.ToBase64String(hash);
          // Constant-time comparison to avoid timing attacks
          return CryptographicOperations.FixedTimeEquals(
              Encoding.UTF8.GetBytes(expected),
              Encoding.UTF8.GetBytes(receivedSignature));
      }
  }
  ```
</CodeGroup>

***

## Legacy method — plain SHA256

Header to compare: `X-Nox-Signature`

<Warning>
  This is the **legacy** method. It may still appear on any integration, so keep supporting it — but always prefer `X-Signature` (HMAC-SHA256) and don't build new implementations around this method.
</Warning>

### Formula

```text theme={null}
Base64( SHA256( webhook_secret + raw_body ) )
```

Here the secret is simply prepended to the body, and the hash is computed over that concatenation.

### Code examples

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import base64
  import hmac

  def validate_legacy_signature(raw_body: bytes, webhook_secret: str, received_signature: str) -> bool:
      content = webhook_secret.encode("utf-8") + raw_body
      hashed = hashlib.sha256(content).digest()
      expected = base64.b64encode(hashed).decode()
      return hmac.compare_digest(expected, received_signature)
  ```

  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function validateLegacySignature(rawBody, webhookSecret, receivedSignature) {
    const expected = crypto
      .createHash("sha256")
      .update(webhookSecret + rawBody, "utf8")
      .digest("base64");

    const a = Buffer.from(expected);
    const b = Buffer.from(receivedSignature);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

  ```go Go theme={null}
  package webhook

  import (
      "crypto/sha256"
      "crypto/subtle"
      "encoding/base64"
  )

  func ValidateLegacySignature(rawBody, webhookSecret, receivedSignature string) bool {
      sum := sha256.Sum256([]byte(webhookSecret + rawBody))
      expected := base64.StdEncoding.EncodeToString(sum[:])
      return subtle.ConstantTimeCompare([]byte(expected), []byte(receivedSignature)) == 1
  }
  ```
</CodeGroup>

<Accordion title="Why is HMAC-SHA256 preferable to plain SHA256?">
  **Plain SHA256** is just a generic hash function. In the legacy method, security comes from prepending the secret to the body (`SHA256(webhook_secret + raw_body)`). The problem is that SHA256 was not designed to be used as a message authentication code: in certain scenarios it is vulnerable to a *length extension attack*, where it's possible to compute a new valid hash without knowing the full secret.

  **HMAC-SHA256** was built specifically for this purpose. It applies the hash in two layers, mixing the key in different ways in each one. This structure closes the *length extension attack* gap and is mathematically proven secure for message authentication — which is why it's the industry standard for signing webhooks.

  In short: plain SHA256 is like taping a password to the front of a document and scrambling everything together. HMAC uses the key as a structured, well-tested way to scramble the content — much harder to defeat.
</Accordion>

***

## Critical points

Apply to **both** methods.

### 1. Use the actual raw body (the most important one)

The signature depends on the **exact bytes** of the body. Any alteration breaks validation — including whitespace, line breaks, key order, or re-serializing the JSON.

<CodeGroup>
  ```text Wrong theme={null}
  Parsing the JSON and reformatting it before validating
  ```

  ```text Correct theme={null}
  Using the raw body exactly as it was received
  ```
</CodeGroup>

<Tip>
  A webhook signature **doesn't validate JSON — it validates exact bytes.** Capture the raw body before any middleware that parses the request (e.g. `express.raw()` in Express, or the raw `await request.body()` in FastAPI).
</Tip>

### 2. Encoding

Always use **UTF-8**, at every step.

***

## Debug / Troubleshooting

If the signature doesn't match, check in this order:

* The **raw body** wasn't altered (cause #1)
* The **correct method** is being used (HMAC for `X-Signature`, legacy SHA256 for `X-Nox-Signature`)
* The **Webhook Secret** is correct
* The **encoding** is UTF-8
* The **correct header** is being read

<Tip>
  In a secure environment, log the **expected** vs. **received** signature side by side. The difference usually points straight to the cause.
</Tip>
