> For the complete documentation index, see [llms.txt](https://skymerse.gitbook.io/notamify-api/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://skymerse.gitbook.io/notamify-api/notam-watcher/webhook-security.md).

# Webhook Security

### Overview

Notamify signs every webhook request with a user‑specific secret so your server can verify authenticity and integrity. The signature is sent in the X-Notamify-Signature header.

The webhook secret described here authenticates received webhook messages only. To authenticate requests to the Watcher API, use the Notamify API key described in the [Authentication guide](/notamify-api/basics/authentication-guide.md).

### How It Works

Each webhook request includes a signature header:

`X-Notamify-Signature: t=<timestamp>,v1=<hex_signature>[,v1=<prev_signature>]`

The signature is computed as:

`HMAC_SHA256(secret, "<timestamp>.<raw_body>")`

* timestamp is seconds since Unix epoch (UTC).
* raw\_body is the exact request body bytes.
* secret is your webhook\_secret.

#### Rotation behavior

When you rotate your webhook secret, Notamify includes two signatures for a short grace period (3 hours):

`X-Notamify-Signature: t=...,v1=<new_sig>,v1=<prev_sig>`

This lets clients verify with either secret during rollout.

<details>

<summary>Signature Breakdown</summary>

**1) Secret (stored by client)**

`nmf_wh_EXAMPLE_SECRET` (placeholder; use your own webhook secret)

**2) Body (raw JSON)**

`{"listener_id":"abc","notam":{"id":"A1234/25"},"delivered_at":"2026-02-05T12:00:00Z"}`

**3) Timestamp**

`1700000000`

**4) Message to sign**

`1700000000.{"listener_id":"abc","notam":{"id":"A1234/25"},"delivered_at":"2026-02-05T12:00:00Z"}`

**5) Signature (HMAC-SHA256, hex)**

`3b4f2a6c9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9`

**6) Header sent by Notamify**

`X-Notamify-Signature: t=1700000000,v1=3b4f2a6c9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa9`

**7) Rotation case (grace window)**

`X-Notamify-Signature: t=1700000000,v1=<new_sig>,v1=<prev_sig>`

</details>

### Getting Your Webhook Secret

You can generate your webhook secret in [Notamify API Manager](https://notamify.com/api-manager).

<figure><img src="/files/Ys9lKURhkCjPBfZaz5Hu" alt=""><figcaption></figcaption></figure>

You can rotate the webhook key at any time. After rotation, the previous key remains active for up to three hours or until the next rotation, whichever comes first.

<figure><img src="/files/OsCPU39xwsAHafdEQf2G" alt=""><figcaption></figcaption></figure>

#### Create or rotate your secret with API

You can also use dedicated endpoint to generate it.

## Rotate webhook secret

> Generates a new webhook secret for the authenticated user. The previous secret remains valid for a short overlap window.

```json
{"openapi":"3.1.0","info":{"title":"Notamify Watcher API","version":"1.0.0"},"servers":[{"url":"https://watcher.notamify.com"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}},"paths":{"/webhook-secret:rotate":{"post":{"summary":"Rotate webhook secret","description":"Generates a new webhook secret for the authenticated user. The previous secret remains valid for a short overlap window.","responses":{"200":{"description":"Webhook secret rotated","content":{"application/json":{"schema":{"type":"object","properties":{"webhook_secret":{"type":"string"}}}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal Server Error"}}}}}}
```

### Verification Steps

1. Read the X-Notamify-Signature header.
2. Parse t= and all v1= values.
3. Compute:

   `expected = HMAC_SHA256(secret, "<timestamp>.<raw_body>")`
4. Compare expected to each v1 value.
5. Enforce a timestamp tolerance (recommended: 10 minutes).

### Python Example (using the SDK)

The [Notamify Python SDK](https://github.com/skymerse/notamify-sdk-python) provides a built-in `verify_signature` function:

```python
from notamify_sdk import verify_signature, SignatureVerificationError

try:
    verify_signature(
        header=request.headers["X-Notamify-Signature"],
        secret="nmf_wh_your_webhook_secret",
        body=request.body,            # raw bytes
        tolerance_seconds=600,        # default: 10 minutes
    )
    print("Signature valid")
except SignatureVerificationError as e:
    print(f"Verification failed: {e}")
```

### Python Example (manual)

```python
import hmac
import hashlib
import time

def parse_signature_header(header: str):
  parts = [p.strip() for p in header.split(",") if p.strip()]
  timestamp = None
  sigs = []
  for part in parts:
      if part.startswith("t="):
          timestamp = part[2:]
      elif part.startswith("v1="):
          sigs.append(part[3:])
  return timestamp, sigs

def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 600):
  if not signature_header:
      return False, "missing signature header"

  timestamp, sigs = parse_signature_header(signature_header)
  if not timestamp or not sigs:
      return False, "invalid signature header"

  try:
      ts = int(timestamp)
  except ValueError:
      return False, "invalid timestamp"

  now = int(time.time())
  if abs(now - ts) > tolerance_seconds:
      return False, "timestamp outside tolerance"

  payload = f"{timestamp}.".encode() + raw_body
  expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()

  if expected not in sigs:
      return False, "signature mismatch"

  return True, "ok"
```

### JavaScript (Node.js) Example

```javascript
const crypto = require("crypto");

function parseSignatureHeader(header) {
  const parts = header.split(",").map(p => p.trim()).filter(Boolean);
  let timestamp = null;
  const sigs = [];
  for (const part of parts) {
    if (part.startsWith("t=")) timestamp = part.slice(2);
    if (part.startsWith("v1=")) sigs.push(part.slice(3));
  }
  return { timestamp, sigs };
}

function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 600) {
  if (!signatureHeader) return { ok: false, error: "missing signature header" };

  const { timestamp, sigs } = parseSignatureHeader(signatureHeader);
  if (!timestamp || sigs.length === 0) return { ok: false, error: "invalid signature header" };

  const ts = Number(timestamp);
  if (!Number.isInteger(ts)) return { ok: false, error: "invalid timestamp" };

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > toleranceSeconds) {
    return { ok: false, error: "timestamp outside tolerance" };
  }

  const payload = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]);
  const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");

  if (!sigs.includes(expected)) return { ok: false, error: "signature mismatch" };

  return { ok: true };
}
```

### Best Practices

* Store the secret securely (never in client‑side code).
* Verify signatures on every request.
* Enforce timestamp tolerance (5–10 minutes).
* Rotate if you suspect compromise.
