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

# Signing & verification

> Verify webhook deliveries with HMAC-SHA256. Always verify — never trust the URL alone.

Every webhook delivery is signed with HMAC-SHA256 using the secret you received when you created the endpoint. **Always verify the signature before processing the payload.** Without verification, anyone who knows your URL can impersonate Ontora.

## How signing works

Ontora computes:

```
signature = HMAC-SHA256(secret, raw_request_body)
```

…and sends it in the `X-Ontora-Signature` header, prefixed with `sha256=`:

```
X-Ontora-Signature: sha256=8b1a9953c4611296a827abf8c47804d7
```

The body is JSON serialized with **no whitespace** between separators (`,` and `:`). Verify against the **raw bytes** of the request body — re-serializing the parsed JSON will produce a different hash and fail verification.

## Verification examples

<CodeGroup>
  ```python Python (FastAPI) theme={null}
  import hmac, hashlib
  from fastapi import FastAPI, Request, HTTPException

  WEBHOOK_SECRET = b"whsec_..."

  app = FastAPI()

  @app.post("/webhooks/ontora")
  async def receive(request: Request):
      body = await request.body()
      sent = request.headers.get("X-Ontora-Signature", "")
      expected = "sha256=" + hmac.new(WEBHOOK_SECRET, body, hashlib.sha256).hexdigest()
      if not hmac.compare_digest(sent, expected):
          raise HTTPException(status_code=401, detail="invalid signature")

      payload = await request.json()
      # ... handle payload
      return {"ok": True}
  ```

  ```typescript Node.js (Express) theme={null}
  import crypto from "node:crypto";
  import express from "express";

  const WEBHOOK_SECRET = process.env.ONTORA_WEBHOOK_SECRET!;
  const app = express();

  // Capture the raw body — required for verification.
  app.use("/webhooks/ontora", express.raw({ type: "application/json" }));

  app.post("/webhooks/ontora", (req, res) => {
    const sent = req.header("X-Ontora-Signature") ?? "";
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", WEBHOOK_SECRET).update(req.body).digest("hex");

    if (
      sent.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected))
    ) {
      return res.status(401).send("invalid signature");
    }

    const payload = JSON.parse(req.body.toString());
    // ... handle payload
    res.json({ ok: true });
  });
  ```

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

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

  var webhookSecret = []byte("whsec_...")

  func handler(w http.ResponseWriter, r *http.Request) {
      body, _ := io.ReadAll(r.Body)
      sent := r.Header.Get("X-Ontora-Signature")

      mac := hmac.New(sha256.New, webhookSecret)
      mac.Write(body)
      expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

      if !hmac.Equal([]byte(sent), []byte(expected)) {
          http.Error(w, "invalid signature", http.StatusUnauthorized)
          return
      }

      // ... handle payload
      w.WriteHeader(http.StatusOK)
  }
  ```
</CodeGroup>

## Checklist

* ✅ Use a **constant-time** comparison (`hmac.compare_digest`, `crypto.timingSafeEqual`, `hmac.Equal`). Don't compare with `==`.
* ✅ Verify against the **raw request body**, not a re-serialized parse.
* ✅ Return **200** quickly and process asynchronously if your handler is slow.
* ✅ Be **idempotent** — retries can deliver the same event twice. Use `X-Ontora-Delivery-Id` as a dedupe key.
* ✅ Allow some clock skew when checking the `timestamp` in the payload.

## Rotating the secret

To rotate, delete the old endpoint and register a new one with the same URL and events. There is currently no in-place rotation — this avoids the dual-secret window and keeps verification simple.
