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

# Verify signatures

> Verify the signature on each connection webhook delivery using the Standard Webhooks specification.

# Verify signatures

Every connection webhook delivery is signed so you can confirm it came from Zapier and was not modified in transit. Zapier follows the [Standard Webhooks](https://www.standardwebhooks.com) specification, so you can verify deliveries with any Standard Webhooks library or with a short amount of code.

Always verify the signature before you act on a payload.

## What you need

* The signing `secret` (`whsec_…`) returned when you [created the webhook](/white-label/connection-webhooks/connection-webhooks-quickstart#step-1-register-a-webhook). If you did not store it, delete the webhook and create a new one to get a fresh secret.
* The raw request body, exactly as received. Do not parse and re-serialize it before verifying: JSON re-serialization can change the bytes and break the signature.

## Signature headers

Each delivery includes three headers:

| Header              | Description                                                                           |
| ------------------- | ------------------------------------------------------------------------------------- |
| `webhook-id`        | Unique identifier for the delivery. Use it to deduplicate retries.                    |
| `webhook-timestamp` | Unix timestamp (seconds) when the delivery was sent. Use it to reject old deliveries. |
| `webhook-signature` | One or more space-delimited signatures, each in the form `v1,<base64>`.               |

The signed content is the string `{webhook-id}.{webhook-timestamp}.{raw-body}`. The signature is the base64-encoded HMAC-SHA256 of that string, keyed by the secret with its `whsec_` prefix removed and the remainder base64-decoded.

## Verify with a library (recommended)

The official Standard Webhooks libraries handle signature construction, base64 decoding, and timestamp tolerance for you.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Webhook, WebhookVerificationError } from "standardwebhooks";

  // rawBody must be the raw request body string, not a parsed object.
  const wh = new Webhook(process.env.WEBHOOK_SECRET); // "whsec_…"

  try {
    const event = wh.verify(rawBody, {
      "webhook-id": req.headers["webhook-id"],
      "webhook-timestamp": req.headers["webhook-timestamp"],
      "webhook-signature": req.headers["webhook-signature"],
    });
    // event is the verified, parsed payload.
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      // Verification failed. Respond 400 and do not process the payload.
    }
  }
  ```

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

  # raw_body must be the raw request body bytes or string, not a parsed dict.
  wh = Webhook(os.environ["WEBHOOK_SECRET"])  # "whsec_…"

  try:
      event = wh.verify(raw_body, {
          "webhook-id": headers["webhook-id"],
          "webhook-timestamp": headers["webhook-timestamp"],
          "webhook-signature": headers["webhook-signature"],
      })
      # event is the verified, parsed payload.
  except WebhookVerificationError:
      # Verification failed. Respond 400 and do not process the payload.
      pass
  ```
</CodeGroup>

## Verify manually

If you cannot add a dependency, reconstruct and compare the signature yourself. This mirrors the official libraries: strip the `whsec_` prefix, base64-decode the key, build the signed content, and compare with a constant-time function.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  function verify(
    secret: string,
    headers: Record<string, string>,
    rawBody: string,
  ): boolean {
    const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
    const msgId = headers["webhook-id"];
    const timestamp = headers["webhook-timestamp"];
    const signatureHeader = headers["webhook-signature"];

    const signedContent = `${msgId}.${timestamp}.${rawBody}`;
    const expected = createHmac("sha256", key).update(signedContent).digest();

    // webhook-signature may hold several space-delimited "v1,<base64>" values.
    return signatureHeader.split(" ").some((part) => {
      const [version, sig] = part.split(",");
      if (version !== "v1" || !sig) return false;
      const sigBuf = Buffer.from(sig, "base64");
      return sigBuf.length === expected.length && timingSafeEqual(sigBuf, expected);
    });
  }
  ```

  ```python Python theme={null}
  import base64
  import hashlib
  import hmac

  def verify(secret, headers, raw_body):
      key = base64.b64decode(secret.removeprefix("whsec_") + "==")
      msg_id = headers["webhook-id"]
      timestamp = headers["webhook-timestamp"]
      signature_header = headers["webhook-signature"]

      signed_content = f"{msg_id}.{timestamp}.{raw_body}".encode()
      expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest()).decode()

      # webhook-signature may hold several space-delimited "v1,<base64>" values.
      for part in signature_header.split(" "):
          version, _, sig = part.partition(",")
          if version == "v1" and hmac.compare_digest(sig, expected):
              return True
      return False
  ```
</CodeGroup>

## Reject old deliveries

To limit replay attacks, reject a delivery whose `webhook-timestamp` is too far from the current time. The Standard Webhooks libraries apply a five-minute tolerance by default. If you verify manually, apply the same check.

## Respond to Zapier

* Return a `2xx` status once you have verified and accepted the delivery.
* Return a non-`2xx` status if you cannot process the delivery. A `429` or `5xx` response triggers a retry. For retry and `Retry-After` behavior, go to [Error handling](/white-label/connection-webhooks/error-handling).
* Deduplicate on `webhook-id`: the same event can be delivered more than once, so make your handler idempotent.

## Next steps

* [Event payload reference](/white-label/connection-webhooks/payload-reference): the fields inside a verified event.
* [Error handling](/white-label/connection-webhooks/error-handling): API error codes and delivery behavior.
