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

# Verify webhook requests

> Reject forged and replayed webhook deliveries before they reach your handlers.

Anyone can `POST` to a public URL. Without verification, you can't tell a real Nomos delivery from a forged one, and an intercepted payload could be replayed against you later. To prevent this, every webhook is signed with a secret tied to your endpoint and stamped with the time it was sent. Check both before acting on the event.

<Note>
  Each webhook endpoint has its own signing secret. Find it on the endpoint's
  detail page in the [Nomos dashboard](https://dashboard.nomos.energy/developer)
  under **Developer → Webhooks**.
</Note>

<Steps>
  <Step title="Parse the header">
    Each request includes an `X-Nomos-Signature` header:

    ```
    t=1768473000,v1=3523dcc0013f08dfa1855772441107330218793f399d7452bd3ff2159c6e0285
    ```

    * `t`: Unix timestamp (seconds) when Nomos sent the request.
    * `v1`: HMAC-SHA256 of `${t}.${rawBody}`, hex-encoded, signed with your endpoint's secret.

    Pull `t` and `v1` out of the header.
  </Step>

  <Step title="Reject stale timestamps">
    If `t` is more than 5 minutes off your clock, treat it as a replay and stop.
  </Step>

  <Step title="Recompute the signature">
    Run `HMAC-SHA256(${t}.${rawBody}, secret)` using your stored signing secret
    and the raw request body, byte-for-byte.
  </Step>

  <Step title="Compare the signatures">
    Compare your computed value to `v1` and reject if they differ. Use a
    timing-safe helper (like Node's `timingSafeEqual`) instead of `===`, so an
    attacker can't learn the signature from how long the comparison takes.
  </Step>
</Steps>

## Example

<CodeGroup>
  ```ts Node theme={null}
  import crypto from "node:crypto";

  export function verifyWebhook(
    rawBody: string,
    header: string,
    secret: string,
  ): boolean {
    const { t, v1 } = Object.fromEntries(
      header.split(",").map((p) => p.split("=")),
    );
    if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
    const hmac = crypto.createHmac("sha256", secret);
    const expected = hmac.update(`${t}.${rawBody}`).digest("hex");
    if (v1.length !== expected.length) return false;
    return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import time


  def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool:
      parts = dict(p.split("=") for p in header.split(","))
      t, v1 = int(parts["t"]), parts["v1"]
      if abs(time.time() - t) > 300:
          return False
      expected = hmac.new(
          secret.encode(),
          f"{t}.".encode() + raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(v1, expected)
  ```
</CodeGroup>

Pass the request body before any JSON parsing. Re-serializing a parsed body can change whitespace or key order, which breaks the signature.
