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

# Verify signatures

> Authenticate exact request bytes and handle the 24-hour rotation overlap.

## The signed request

Every request includes:

```text theme={null}
Content-Type: application/json
FloKit-Signature: t=<unix_milliseconds>,v1=<lowercase_hex_hmac>[,v1=<second_hex_hmac>]
FloKit-Event-Id: <event_id>
FloKit-Delivery-Id: <delivery_id>
FloKit-Attempt: <scheduled_attempt>
Idempotency-Key: <event_id>
```

The signature follows the Appcharge v2 generation design: `HMAC-SHA256(secret, timestamp + "." + JSON payload)`, using a Unix timestamp in milliseconds and lowercase hexadecimal output. FloKit signs the exact JSON bytes sent on the wire, which avoids ambiguity from parsing and re-serializing JSON. Use the entire `flk_whsec_...` secret string as the HMAC key. Read the original request bytes before JSON middleware changes them. Verify first, then decode and validate JSON. Treat IDs in the signed body as authority; the separate ID headers help diagnostics.

The body and event ID remain the same across retries. FloKit creates a fresh timestamp and signature for every physical send. Permit at most five minutes of clock difference in either direction and keep your server clock synchronized.

## Copy the verifier

Both examples accept a list containing your currently configured secret and, while updating your receiver during rotation, the previous secret. They accept any matching `v1` value, reject malformed headers and oversized requests, and use constant-time digest comparisons.

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

  export function verifyFloKit(
    rawBody: Uint8Array,
    header: string | null | undefined,
    secrets: readonly string[],
    nowMilliseconds = Date.now(),
  ): boolean {
    if (rawBody.byteLength > 262144 || !header || header.length > 1024
      || !Number.isSafeInteger(nowMilliseconds) || nowMilliseconds < 0
      || secrets.length < 1 || secrets.length > 2
      || secrets.some((secret) => !secret || secret.length > 4096)) return false;
    let timestamp: number | undefined;
    const signatures: Buffer[] = [];
    for (const part of header.split(',')) {
      const field = /^(t|v1)=([^\s,]+)$/.exec(part.trim());
      if (!field) return false;
      const value = field[2]!;
      if (field[1] === 't') {
        if (timestamp !== undefined || !/^(0|[1-9][0-9]{0,15})$/.test(value)) return false;
        timestamp = Number(value);
        if (!Number.isSafeInteger(timestamp)) return false;
      } else {
        if (!/^[0-9a-f]{64}$/.test(value) || signatures.length >= 8) return false;
        signatures.push(Buffer.from(value, 'hex'));
      }
    }
    if (timestamp === undefined || signatures.length === 0 || Math.abs(nowMilliseconds - timestamp) > 300000) return false;
    let matched = 0;
    for (const secret of secrets) {
      const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
      for (const signature of signatures) matched |= Number(timingSafeEqual(expected, signature));
    }
    return matched === 1;
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import re
  import time


  def verify_flokit(raw_body: bytes, header: str, secrets: list[str], now_milliseconds=None) -> bool:
      now = int(time.time() * 1000) if now_milliseconds is None else now_milliseconds
      if (len(raw_body) > 262144 or not header or len(header) > 1024
              or type(now) is not int or now < 0 or now > 9007199254740991
              or not 1 <= len(secrets) <= 2
              or any(not secret or len(secret) > 4096 for secret in secrets)):
          return False
      timestamp = None
      signatures = []
      for part in header.split(','):
          field = re.fullmatch(r'(t|v1)=([^\s,]+)', part.strip())
          if field is None:
              return False
          key, value = field.groups()
          if key == 't':
              if timestamp is not None or re.fullmatch(r'(0|[1-9][0-9]{0,15})', value) is None:
                  return False
              timestamp = int(value)
              if timestamp > 9007199254740991:
                  return False
          else:
              if re.fullmatch(r'[0-9a-f]{64}', value) is None or len(signatures) >= 8:
                  return False
              signatures.append(bytes.fromhex(value))
      if timestamp is None or not signatures or abs(now - timestamp) > 300000:
          return False
      signed = str(timestamp).encode('ascii') + b'.' + raw_body
      matched = False
      for secret in secrets:
          expected = hmac.new(secret.encode('utf-8'), signed, hashlib.sha256).digest()
          for signature in signatures:
              matched = bool(matched | hmac.compare_digest(expected, signature))
      return matched
  ```
</CodeGroup>

For Node.js JavaScript, compile the TypeScript example with `tsc` or remove its type annotations. No provider SDK is required. The verification helper is tested against the same raw-byte fixtures as FloKit's sender.

## Minimal raw-request diagnostic

For a Node `IncomingMessage`, read bounded chunks into a `Buffer`, pass that buffer and the single `flokit-signature` header to `verifyFloKit`, and parse JSON only on success. Set request/header timeouts on your HTTP server and reject a second signature-header field.

```typescript Raw request handling theme={null}
import type { IncomingMessage } from 'node:http';

export async function readRawBody(request: IncomingMessage): Promise<Buffer> {
  const chunks: Buffer[] = [];
  let size = 0;
  for await (const chunk of request) {
    const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
    size += bytes.length;
    if (size > 262144) throw new Error('Request body too large');
    chunks.push(bytes);
  }
  return Buffer.concat(chunks);
}
```

Log only the verification verdict, response code, duration, and safe opaque identifiers. Do not log the body, email, answers, secret, or signing header. A successful signature authenticates the bytes; durable acceptance still requires the [unique event transaction](/webhooks/setup#accept-durably).

For an isolated **unsigned local diagnostic endpoint only**, this checks basic HTTP connectivity. It does not test verification and must fail authentication at a production receiver:

```bash theme={null}
curl --request POST http://127.0.0.1:8080/unsigned-diagnostic \
  --header 'Content-Type: application/json' \
  --data-binary '{"test_event":true}'
```

## Rotate a secret

Use **Rotate signing secret** in Admin and confirm the operation. Install the new secret in every receiver for that Company. FloKit signs with both active and retiring secrets during the 24-hour overlap. After expiry, FloKit uses only the new active secret; remove the retired secret from your receiver. Rotation does not change event IDs or frozen request bodies.
