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

# Verify Webhook HMAC Signature

> Verify Suki webhook requests with HMAC-SHA-256 on the raw body before you parse JSON

**Problem:** Webhook signature checks fail, or you process unverified POSTs after framework JSON parsing rewrites the body.

**Solution:** Sign `generated-at` + `:` + the raw body with HMAC-SHA-256 using your partner secret. Compare the hex digest to `X-API-Key` with a constant-time compare.

<Note>
  This cookbook assumes you already have your webhook secret key from partner onboarding and a HTTPS callback URL.
</Note>

<Tabs>
  <Tab title="Python">
    ```python theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    import hashlib
    import hmac
    import json

    from flask import Flask, jsonify, request

    app = Flask(__name__)
    secret_key = "<partner_secret_key>"

    @app.route("/webhooks/notification", methods=["POST"])
    def handle_webhook():
        raw_body = request.get_data(as_text=True)
        timestamp = request.headers.get("generated-at", "")
        message = timestamp + ":" + raw_body

        expected = request.headers.get("X-API-Key", "")
        computed = hmac.new(
            secret_key.encode("utf-8"),
            message.encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()

        if not hmac.compare_digest(computed, expected):
            return jsonify({"error": "Invalid signature"}), 401

        data = json.loads(raw_body)
        return jsonify({"message": "Notification received"}), 200
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
    import crypto from "crypto";
    import express from "express";

    const app = express();
    const secretKey = "<partner_secret_key>";

    app.post(
      "/webhooks/notification",
      express.raw({ type: "application/json" }),
      (req, res) => {
        const rawBody = req.body.toString("utf8");
        const timestamp = String(req.headers["generated-at"] || "");
        const message = timestamp + ":" + rawBody;

        const expected = String(req.headers["x-api-key"] || "");
        const computed = crypto
          .createHmac("sha256", secretKey)
          .update(message, "utf8")
          .digest("hex");

        const match =
          computed.length === expected.length &&
          crypto.timingSafeEqual(
            Buffer.from(computed, "utf8"),
            Buffer.from(expected, "utf8")
          );

        if (!match) {
          return res.status(401).json({ error: "Invalid signature" });
        }

        const data = JSON.parse(rawBody);
        return res.status(200).json({ message: "Notification received" });
      }
    );
    ```
  </Tab>
</Tabs>

## Common mistakes

* Auto-parsing JSON before you read the raw body breaks verification.
* Do not trim, pretty-print, or re-serialize the body before you sign it.

## Other cookbooks

<div className="cookbook-hub-wrap">
  <div className="hp-io-method-grid tut-hub-card-grid" data-cookbook-related-grid>
    <a className="hp-io-method-card tut-hub-method-card" href="/documentation/cookbooks/wait-for-status-before-retrieve">
      <div className="tut-hub-card-media tut-hub-card-media--blue" aria-hidden="true" />

      <div className="hp-io-method-card-body">
        <div className="tut-hub-card-badges">
          <span className="hp-wn-badge hp-wn-badge-new">Ambient</span>
          <span className="hp-wn-badge cookbook-hub-badge-surface cookbook-hub-badge-surface--api">API</span>
        </div>

        <h3 className="hp-io-method-card-title">Poll Session Status Before Fetching Content</h3>

        <p className="hp-io-method-card-desc cookbook-hub-card-desc">
          Poll until status is completed.
        </p>

        <div className="hp-io-method-card-meta tut-hub-card-foot" aria-label="5 min">
          <div className="tut-hub-card-foot-meta">
            <span className="hp-io-method-card-meta-time">5 min</span>
          </div>
        </div>
      </div>
    </a>

    <a className="hp-io-method-card tut-hub-method-card" href="/documentation/cookbooks/get-note-content-by-composition-id">
      <div className="tut-hub-card-media" aria-hidden="true" />

      <div className="hp-io-method-card-body">
        <div className="tut-hub-card-badges">
          <span className="hp-wn-badge hp-wn-badge-new">Ambient</span>
          <span className="hp-wn-badge cookbook-hub-badge-surface cookbook-hub-badge-surface--api">API</span>
        </div>

        <h3 className="hp-io-method-card-title">Fetch Note Content with composition\_id</h3>

        <p className="hp-io-method-card-desc cookbook-hub-card-desc">
          Retrieve the note with composition\_id.
        </p>

        <div className="hp-io-method-card-meta tut-hub-card-foot" aria-label="5 min">
          <div className="tut-hub-card-foot-meta">
            <span className="hp-io-method-card-meta-time">5 min</span>
          </div>
        </div>
      </div>
    </a>
  </div>
</div>
