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

# Verifying signatures

> Confirm that a webhook delivery genuinely came from Audienceful.

Every delivery is signed so you can confirm it genuinely came from Audienceful and wasn't tampered with or replayed. Verify the signature on each incoming request **before** trusting its contents.

## The signature header

Each delivery carries an `X-Audienceful-Signature` header containing a timestamp and a signature:

```
X-Audienceful-Signature: t=1751630400,v1=<hex hmac-sha256>
```

* `t` — the Unix timestamp (seconds) when the delivery was signed.
* `v1` — the signature: the lowercase hex `HMAC-SHA256` of the string `{t}.{raw_request_body}`, keyed with your endpoint's signing `secret`.

## How to verify

1. Read the raw request body and the `X-Audienceful-Signature` header.
2. Parse `t` and `v1` out of the header.
3. Reject the request if `t` is more than **5 minutes** away from your current time (replay protection).
4. Compute `HMAC-SHA256(secret, "{t}.{raw_body}")` as lowercase hex.
5. Compare your computed value against `v1` using a **constant-time** comparison. If they match, the request is authentic.

Your signing `secret` (`whsec_…`) is returned once, when you [create the endpoint](/api-reference/webhooks/create).

<Warning>Verify against the **raw request body bytes**, exactly as received — before any JSON parsing or re-serialization. Re-serializing the body (even reformatting whitespace) changes it, and the signature will no longer match. Most frameworks expose the raw body separately from the parsed JSON.</Warning>

## Examples

<CodeGroup>
  ```python Python theme={null}
  import hashlib, hmac, time


  def verify_signature(secret: str, raw_body: str, signature_header: str, tolerance: int = 300) -> bool:
      parts = dict(kv.split("=", 1) for kv in signature_header.split(","))
      timestamp = int(parts["t"])

      # Reject stale timestamps (replay protection).
      if abs(time.time() - timestamp) > tolerance:
          return False

      expected = hmac.new(
          secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, parts["v1"])
  ```

  ```typescript TypeScript theme={null}
  import crypto from "crypto";

  export function verifySignature(
    secret: string,
    rawBody: string,
    signatureHeader: string,
    toleranceSeconds = 300,
  ): boolean {
    const parts: Record<string, string> = {};
    for (const kv of signatureHeader.split(",")) {
      const [key, value] = kv.split("=", 2);
      parts[key] = value;
    }

    const timestamp = parseInt(parts.t, 10);
    // Reject stale timestamps (replay protection).
    if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
      return false;
    }

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

    const expectedBuf = Buffer.from(expected);
    const providedBuf = Buffer.from(parts.v1 ?? "");
    return (
      expectedBuf.length === providedBuf.length &&
      crypto.timingSafeEqual(expectedBuf, providedBuf)
    );
  }
  ```

  ```php PHP theme={null}
  <?php

  function verify_signature(string $secret, string $rawBody, string $signatureHeader, int $tolerance = 300): bool {
      $parts = [];
      foreach (explode(',', $signatureHeader) as $kv) {
          [$key, $value] = array_pad(explode('=', $kv, 2), 2, '');
          $parts[$key] = $value;
      }

      $timestamp = (int) ($parts['t'] ?? 0);
      // Reject stale timestamps (replay protection).
      if (abs(time() - $timestamp) > $tolerance) {
          return false;
      }

      $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
      return hash_equals($expected, $parts['v1'] ?? '');
  }
  ```
</CodeGroup>

<Tip>The same signature scheme is used by the [automation "Send Webhook" action](/api-reference/webhooks/overview#automation-send-webhook-action), signed with that action's own secret — so the same verification code works there too.</Tip>
