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

# Verifying Signatures

> Confirm that webhook requests genuinely come from Zivio

### Why verify?

Your webhook URL is a public endpoint, so anyone could attempt to send requests to it. Every request from Zivio is **signed** with your webhook's signing secret. By verifying the signature before acting on a request, you can be confident it genuinely came from Zivio and was not tampered with in transit.

<Warning>Always verify the signature before trusting or processing a webhook payload.</Warning>

### Use a Standard Webhooks library (recommended)

Zivio signs requests following the [Standard Webhooks](https://www.standardwebhooks.com/) specification. The easiest and safest way to verify is with one of the [official open-source libraries](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries), available for many languages. Pass your signing secret and the request headers, and the library handles verification — including protection against replay attacks — for you.

<CodeGroup>
  ```javascript Node.js theme={null}
  import { Webhook } from "standardwebhooks";

  const wh = new Webhook(process.env.ZIVIO_WEBHOOK_SECRET); // "whsec_..."

  // `body` must be the raw request body string, not a parsed object
  const payload = wh.verify(body, {
    "webhook-id": req.headers["webhook-id"],
    "webhook-timestamp": req.headers["webhook-timestamp"],
    "webhook-signature": req.headers["webhook-signature"],
  });

  // `payload` is now the verified, parsed event
  ```

  ```python Python theme={null}
  from standardwebhooks import Webhook

  wh = Webhook(os.environ["ZIVIO_WEBHOOK_SECRET"])  # "whsec_..."

  # `body` must be the raw request body bytes/string, not a parsed object
  payload = wh.verify(body, {
      "webhook-id": headers["webhook-id"],
      "webhook-timestamp": headers["webhook-timestamp"],
      "webhook-signature": headers["webhook-signature"],
  })
  ```
</CodeGroup>

### How the signature works

If you prefer to verify manually, the scheme is straightforward:

<Steps>
  <Step title="Build the signed content">
    Concatenate the `webhook-id`, the `webhook-timestamp`, and the raw request body, separated by full stops:

    ```
    {webhook-id}.{webhook-timestamp}.{raw-body}
    ```
  </Step>

  <Step title="Compute an HMAC">
    Your signing secret is the part after the `whsec_` prefix, which is **Base64-encoded**. Base64-decode it to get the key, then compute an `HMAC-SHA256` of the signed content using that key. Base64-encode the result.
  </Step>

  <Step title="Compare against the header">
    The `webhook-signature` header contains a space-separated list of signatures, each prefixed with a version, e.g. `v1,<base64-signature>`. Compare your computed value against the `v1` signature using a constant-time comparison.
  </Step>

  <Step title="Check the timestamp">
    Reject requests whose `webhook-timestamp` is too far from the current time (a tolerance of around five minutes is typical) to defend against replay attacks.
  </Step>
</Steps>

<Note>You must use the **raw** request body exactly as received to compute the signature. Re-serializing a parsed JSON object can change whitespace or key order and will cause verification to fail.</Note>

### Manual verification example

```javascript Node.js theme={null}
import crypto from "crypto";

function verify(rawBody, headers, signingSecret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatureHeader = headers["webhook-signature"];

  // 1. Build the signed content
  const signedContent = `${id}.${timestamp}.${rawBody}`;

  // 2. Compute the expected signature
  const key = Buffer.from(signingSecret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(signedContent)
    .digest("base64");

  // 3. Compare against each signature in the header (scheme: "v1,<sig>")
  const passed = signatureHeader.split(" ").some((part) => {
    const [, sig] = part.split(",");
    return (
      sig &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
    );
  });

  if (!passed) throw new Error("Invalid webhook signature");
}
```

### Rotating your secret

If your secret is ever exposed, rotate it from the Webhooks admin. Rotation takes effect **immediately**: the old secret stops working at once and a new one is shown to you a single time. Update the stored secret in your endpoint as soon as you rotate, or verification will fail in the interim.
