> ## 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.

# Webhook Signature Verification

> Verify Webhook request signatures with HMAC, X-API-Key, and generated-at headers before processing callback payloads

When Suki makes a webhook request to your callback URL, it includes a **signature** that allows your application to verify that the request originated from Suki and that the request body was not modified in transit.

<Warning>
  Verify the Webhook request signature before parsing the JSON request body. Processing unverified requests can expose your integration to spoofed requests and tampered payloads.
</Warning>

## What you need to verify the Webhook request signature

To verify the Webhook request signature, you need the following:

| Component                 | Description                                                                                                                                                                                      |
| :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Secret key**            | A password-like string Suki puts on your **partner record** and shares with you during [partner onboarding](/documentation/get-started/partner-onboarding). Only your **server** should know it. |
| **`generated-at` header** | A number: **Unix time in milliseconds** when Suki built this request. You need it as part of the signed text.                                                                                    |
| **`X-API-Key` header**    | The **expected signature** for this request, already turned into **hex** text. Your job is to compute the same hex and see if they are equal.                                                    |

Suki sends each notification as a <Badge color="blue" size="sm">POST</Badge> with **`Content-Type: application/json`**. Your handler reads the raw body and the two headers above before you trust the payload.

## How to verify the Webhook request signature on your server

Perform the following steps on your application server to verify the Webhook request signature:

<Steps>
  <Step title="Read the Raw Body First" icon="file-lines">
    Read the request body **before** you parse JSON. Keep the exact bytes or string Suki sent (no pretty-printing, no trimming, no changing spaces).
  </Step>

  <Step title="Read the Generated-At Header" icon="clock">
    Read the **`generated-at`** header exactly as the string Suki sent (the millisecond timestamp).

    <Warning>
      The webhook rejects requests where the `generated-at` timestamp is older than two minutes to prevent replay attacks.
    </Warning>
  </Step>

  <Step title="Build the String to Sign" icon="link">
    Join **`generated-at` + `:` + raw body** into one long string. Example shape: `1765977748432:{"status":"success",...}` (your timestamp and JSON will differ).
  </Step>

  <Step title="Run HMAC-SHA-256" icon="lock">
    Run **HMAC-SHA-256** using your **secret key** as the key and that long string as the data. **HMAC-SHA-256** is a standard "sign this text with this secret" operation; every language has a library for it.
  </Step>

  <Step title="Encode the Digest as Hex" icon="code">
    Turn the HMAC output into **hex** the same way Suki does (usually lowercase hex; if your comparison fails, ask your Suki contact whether casing matters).
  </Step>

  <Step title="Compare to X-API-Key" icon="check">
    Compare your hex to the **`X-API-Key`** header. Use a **constant-time** compare if your framework offers one, so attackers cannot guess the signature byte by byte.
  </Step>
</Steps>

If the values **do not match**, stop and return **4xx**. If they **match**, you can safely **parse the JSON** and run your business logic.

**Pseudocode**

```text theme={"theme":{"light":"github-dark","dark":"material-theme-darker"}}
raw_body = read request body as received (string or bytes)
timestamp = request header "generated-at" exactly as sent
message = timestamp + ":" + raw_body

expected_signature_hex = request header "X-API-Key"
computed_signature_hex = hex( HMAC_SHA256(key = secret_key, data = message) )

if not constant_time_equal(computed_signature_hex, expected_signature_hex):
    return 401 or 400
# else: parse JSON and continue
```

<Tip>
  Read the **raw request body** before JSON parsing. Framework middleware that auto-parses JSON first will break verification because the signature covers the exact bytes Suki sent.
</Tip>

## Code examples

The examples below follow the pseudocode above line for line.

**Language tabs (agents):** Equivalent code samples are available in: Python, TypeScript. Humans see one language at a time. Use the variant that matches the user's stack; behavior is the same across tabs.

<Tabs>
  <Tab title="Python">
    ```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__)

    # Partner secret key from onboarding
    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_signature_hex = request.headers.get("X-API-Key", "")
        computed_signature_hex = hmac.new(
            secret_key.encode("utf-8"),
            message.encode("utf-8"),
            hashlib.sha256,
        ).hexdigest()

        if not hmac.compare_digest(computed_signature_hex, expected_signature_hex):
            return jsonify({"error": "Invalid signature"}), 401  # or 400

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

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

    const app = express();

    // Partner secret key from onboarding
    const secret_key = "<partner_secret_key>";

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

      const expected_signature_hex = req.headers["x-api-key"] as string;
      const computed_signature_hex = crypto
        .createHmac("sha256", secret_key)
        .update(message, "utf8")
        .digest("hex");

      const signatures_match =
        computed_signature_hex.length === expected_signature_hex.length &&
        crypto.timingSafeEqual(
          Buffer.from(computed_signature_hex, "utf8"),
          Buffer.from(expected_signature_hex, "utf8"),
        );

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

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

For a full handler that branches on **`status`** and processes **`_links`**, refer to the [Asynchronous notifications (Webhook)](/api-reference/asynchronous/webhook.mdx) API reference.

## After verification

After verifying the Webhook signature, parse the JSON request body and handle the notification based on its type:

1. **Ambient session notifications:** Read the top-level **`status`** field to determine the session outcome. Use the **`session_id`**, **`encounter_id`**, and **`_links`** fields to process the event.
2. **CKG data ingestion notifications:** Read the **`state`** field to determine the ingestion outcome. Use the **`transaction_id`** and **`correlation_id`** fields to identify and track the ingestion job.

For the complete payload schema and field descriptions, refer to [Payload & response](/documentation/webhook/payload-and-response) guide.

<Note>
  Return a **2xx** response (for example, **200 OK**) after successfully receiving and validating the webhook. This tells Suki the notification was delivered. Process any follow-up work, such as API calls or database updates, after returning the response.
</Note>

## Security best practices

<AccordionGroup>
  <Accordion icon="lock" title="Verify Requests Are from Suki">
    Implement **HMAC-SHA-256** verification using your **secret key**, the **`generated-at`** header, the raw body, and the **`X-API-Key`** header as described above.
  </Accordion>

  <Accordion icon="lock" title="Validate the Payload">
    After the signature matches, check the JSON shape and **`status`**. Reject malformed or unverified requests with an appropriate **4xx** response.
  </Accordion>

  <Accordion icon="lock" title="Store the Secret Key Securely">
    Keep the partner secret key in a secrets manager (for example AWS Secrets Manager or Azure Key Vault), not in source control.
  </Accordion>

  <Accordion icon="lock" title="Use HTTPS Only">
    Your callback URL must use **HTTPS** with **TLS 1.2** or higher. Suki will not send Webhooks to **HTTP** URLs. See [Configuration](/documentation/webhook/configuration).
  </Accordion>
</AccordionGroup>

For platform-wide guidance, refer to [Security & best practices](/api-reference/security-best-practices#webhook-security) and [Authentication FAQs](/documentation/references/faqs/authentication).

## Available cookbooks

<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/verify-webhook-hmac-signature">
    <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">Webhooks</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">Verify Webhook HMAC Signature</h3>

      <p className="hp-io-method-card-desc cookbook-hub-card-desc">
        Verify HMAC before parsing JSON.
      </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>

## Available tutorials

<div className="hp-io-method-grid tut-hub-card-grid">
  <a className="hp-io-method-card tut-hub-method-card" href="/documentation/tutorials/webhook-notification-receiver">
    <div className="tut-hub-card-media" aria-hidden="true" />

    <div className="hp-io-method-card-body">
      <span className="hp-wn-badge hp-wn-badge-new">Webhooks</span>
      <h3 className="hp-io-method-card-title">Build a Webhook Notification Receiver</h3>

      <p className="hp-io-method-card-desc">
        Verify HMAC signatures, parse partner notifications, and handle success and failure events.
      </p>

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

## Next steps

<Icon icon="file-lines" iconType="solid" /> Refer to [Payload & response](/documentation/webhook/payload-and-response) for payload structure, example JSON bodies, implementation tips, and follow-up API response codes.

<Icon icon="file-lines" iconType="solid" /> Refer to [Event types](/documentation/webhook/event-types) for session completion, failure, timeout, and cancellation events.
