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

# Signing Secrets & Verification

> Sign and verify Sixtyfour webhook deliveries with HMAC-SHA256.

## Use Case

Every outgoing webhook from Sixtyfour can be signed with HMAC-SHA256 so you can verify it actually came from us. One signing secret per organization covers all outgoing webhook traffic: async-job webhooks ([receive results](/api-reference/webhooks/outgoing#source-1-async-job-webhooks)) and the workflow [`outgoing_webhook` block](/api-reference/webhooks/outgoing#source-2-workflow-outgoing_webhook-block).

Until you create a signing secret, deliveries are sent **unsigned** for backward compatibility. Once you create one, all subsequent deliveries are signed.

## Generate a signing secret

Generate a signing secret in the dashboard at [Settings → Webhooks](https://app.sixtyfour.ai/settings/webhooks).

<Warning>The plaintext secret (`sk_whsec_<64 hex>`) is shown only once at creation — store it immediately in a secret manager. Afterwards only a masked preview (`sk_whsec_••••…<last4>`) is displayed.</Warning>

## Headers on signed deliveries

When signing is enabled, every outgoing webhook includes:

| Header                       | Value                                                           |
| ---------------------------- | --------------------------------------------------------------- |
| `Sixtyfour-Signature`        | `t=<unix_seconds>,v1=<hex>[,v1=<hex>]`                          |
| `Sixtyfour-Event-Id`         | UUID per delivery — use as your dedupe key                      |
| `Sixtyfour-Event-Type`       | Routing hint (e.g., `find_email`, `outgoing_webhook.completed`) |
| `Sixtyfour-Delivery-Attempt` | Attempt counter starting at `1`; increments on each retry       |

## Verifying signatures

The signature is `HMAC_SHA256(secret_utf8_bytes, f"{t}.{raw_body}")`. To verify:

1. Parse `t=` (timestamp) and every `v1=` (signature) from the `Sixtyfour-Signature` header.
2. Reject if `|now - t| > 300` (5-minute clock-skew tolerance).
3. Recompute the HMAC against the **raw bytes you received**.
4. Accept if any `v1=` matches.

<Warning>Verify against the raw request body, before JSON parsing. Re-serializing a parsed dict changes the bytes (whitespace, key order) and verification will fail.</Warning>

<Warning>During rotation the header carries two `v1=` segments (new, then old). Iterate every segment — do not parse the header into a dict, which drops duplicates.</Warning>

## Example Webhook Verification

<CodeGroup>
  ```python Python theme={null}
  import hmac, hashlib, time
  from flask import Flask, request, abort

  SECRET = "sk_whsec_..."  # store in a secret manager
  TOLERANCE = 300

  app = Flask(__name__)

  def verify(raw: bytes, header: str, secret: str) -> bool:
      parts = [p.strip() for p in header.split(",")]
      try:
          t = int(next(p.split("=", 1)[1] for p in parts if p.startswith("t=")))
      except (StopIteration, ValueError):
          return False
      if abs(int(time.time()) - t) > TOLERANCE:
          return False
      expected = hmac.new(
          secret.encode("utf-8"),
          f"{t}.".encode("utf-8") + raw,
          hashlib.sha256,
      ).hexdigest()
      return any(
          p.startswith("v1=") and hmac.compare_digest(expected, p.split("=", 1)[1])
          for p in parts
      )

  @app.post("/webhooks/sixtyfour")
  def handle():
      raw = request.get_data()  # raw bytes, before JSON parse
      sig = request.headers.get("Sixtyfour-Signature", "")
      if not verify(raw, sig, SECRET):
          abort(400)
      event = request.get_json()
      return ("", 200)
  ```

  ```javascript JavaScript theme={null}
  import crypto from "crypto";
  import express from "express";

  const SECRET = process.env.SIXTYFOUR_WEBHOOK_SECRET;
  const TOLERANCE = 300;
  const app = express();

  app.use(express.raw({ type: "application/json" }));

  function verify(raw, header, secret) {
    const parts = header.split(",").map((p) => p.trim());
    const tPart = parts.find((p) => p.startsWith("t="));
    if (!tPart) return false;
    const t = parseInt(tPart.slice(2), 10);
    if (!Number.isFinite(t)) return false;
    if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE) return false;
    const expected = crypto
      .createHmac("sha256", secret)
      .update(Buffer.concat([Buffer.from(`${t}.`), raw]))
      .digest("hex");
    return parts.some((p) => {
      if (!p.startsWith("v1=")) return false;
      const got = Buffer.from(p.slice(3), "utf8");
      const exp = Buffer.from(expected, "utf8");
      return got.length === exp.length && crypto.timingSafeEqual(got, exp);
    });
  }

  app.post("/webhooks/sixtyfour", (req, res) => {
    if (!verify(req.body, req.header("Sixtyfour-Signature") || "", SECRET)) {
      return res.status(400).send("invalid signature");
    }
    const event = JSON.parse(req.body.toString("utf8"));
    res.status(200).send("OK");
  });
  ```
</CodeGroup>

## Rotation

Rotate from the dashboard. The default overlap window is **24 hours**: during the window we dual-sign every delivery with both old and new secrets so you can deploy your new secret without dropping events. Set the overlap to **0** for an immediate cut-over (e.g. suspected compromise).

Two rotation rules to remember in your verification code:

* The `Sixtyfour-Signature` header carries multiple `v1=` segments during overlap. You should iterate every segment — accept the delivery if any one matches. See the code examples above for reference.
* You should not parse the header into a dictionary keyed by name — that silently drops the second `v1=`.

## Fail-closed behavior

If signing is configured but we cannot load your secret at delivery time we abort delivery rather than silently downgrade to unsigned. Once you opt into signing, every payload is signed or none are.

If deliveries unexpectedly stop after enabling signing, results remain retrievable through the polling fallbacks:

* Async jobs: `GET /job-status/{task_id}`
* Workflow runs: see [Workflow Execution](/api-reference/workflows/workflows-execution)

## Troubleshooting

For signature verification failures and signing-related delivery issues, see [Handling Errors](/api-reference/errors#issue-webhook-signature-verification-fails).
