# Verify incoming webhooks

> Verify exact request bytes, signed delivery timestamps and secret rotation with the server SDK.


Verify the exact request bytes before parsing JSON or processing an event. The Node.js server helper is available from `@kismet-tech/sdk/server` in source builds containing this change. It is not included in the published `0.1.0-beta.2` package. See the [source-pinned evaluation workflow](https://developers.kismet.travel/guides/private-evaluation/).

```ts
import {
  verifyKismetWebhook,
  KismetWebhookVerificationError,
} from '@kismet-tech/sdk/server';

// Inside your Node.js Fetch-compatible request handler:
const rawBody = new Uint8Array(await request.arrayBuffer());
let verified;
try {
  verified = verifyKismetWebhook({
    rawBody,
    headers: request.headers,
    secret: signingSecretFromServerConfig,
  });
} catch (error) {
  if (error instanceof KismetWebhookVerificationError) {
    return new Response('Invalid webhook', { status: 400 });
  }
  throw error;
}

// Only now parse and validate the event using its agreed version/schema.
const payload: unknown = JSON.parse(new TextDecoder().decode(rawBody));
// Validate the event's environment and authorized application/collection scope.
// Atomically record verified.id with your durable enqueue or business write.
// Acknowledge with 2xx only after that operation commits.
```

`request` is your incoming Request; `signingSecretFromServerConfig` is the destination's secret held on the server. Keep this code in a Node.js route, not a browser component or Edge runtime. Express users should supply the `Buffer` from raw-body middleware, before any JSON middleware. A parsed body re-serialized with `JSON.stringify` is not the original body.

Other server SDK operations, including guest authentication, can run in supported Edge runtimes. The `@kismet-tech/sdk/server` export selects a Web API-only entry for Next.js Edge and Workers; the synchronous webhook verifier is available only in the Node.js entry. In Next.js, use `export const runtime = 'nodejs'` for your webhook receiver.

## Inputs and result

| Field              | Meaning                                                                                                                                                                   |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rawBody`          | Exact bytes as `Uint8Array`/Node `Buffer`, or an exact UTF-8 string. Whitespace, key order, Unicode and trailing newlines matter.                                         |
| `headers`          | Fetch `Headers` or Node-style case-insensitive header record. Required: `webhook-id`, `webhook-timestamp`, `webhook-signature`. Duplicate IDs or timestamps are rejected. |
| `secret`           | Current signing secret, or an array of up to eight active/previous secrets during a bounded rotation window.                                                              |
| `toleranceSeconds` | Nonnegative integer clock tolerance, default 300 seconds. Both excessively old and future timestamps fail. Zero requires the current second.                              |
| `nowSeconds`       | Optional nonnegative integer Unix seconds for deterministic tests. Production should use the local clock default.                                                         |
| Result             | `{ id, timestamp }`, the authenticated header ID and delivery timestamp. No payload parsing, event-type assertion or authorization is implied.                            |

All verification failures use `KismetWebhookVerificationError` with a fixed message. The helper does not log secrets, signatures, headers or bodies. Enforce your request-body size limit before buffering the body. Protect any signing secrets in server configuration; do not include them in URLs, telemetry, error responses or client bundles.

## Existing v1 wire convention

Kismet uses the Standard Webhooks header names and signs the bytes of:

```text
<webhook-id>.<webhook-timestamp>.<exact raw body>
```

The signature is SHA-256 HMAC encoded as padded base64, with header value `v1,<signature>`. **For Kismet's existing v1 signer, remove a single `whsec_` prefix and use the remaining UTF-8 text as the HMAC key. Do not base64-decode or hex-decode that suffix.** This key convention differs from libraries that assume every `whsec_` suffix is base64-encoded key material.

The helper accepts multiple space-separated `v1` signatures, repeated/coalesced signature headers, and multiple configured secrets. It checks recognized signatures with constant-time comparison and accepts a match against any configured key. Unknown signature versions are ignored, but cannot authenticate a request by themselves. It does not fall back to the legacy `X-Kismet-Signature` mirror or accept unsigned deliveries.

Receiver support for rotation does not itself configure sender rotation. Keep a previous secret only for the agreed overlap window and remove it afterward.

## Freshness, retries and deduplication

The signed header timestamp must be the **delivery-attempt time**, including delayed retries and manual replay. The event's occurrence timestamp can remain unchanged. A sender that reuses the original occurrence time for delayed delivery will fail the default freshness check. Confirm per-attempt timestamps before enabling a receiver with this helper; do not solve an incompatible sender by disabling replay protection.

A valid signature inside the freshness window can be replayed. Persist the verified event ID atomically with your work, scoped to the endpoint/installation. Retry of the same event must not cause a second side effect. Do not assume delivery order. A signature establishes authenticity and freshness only: separately validate the event version, type, authorized scope and TEST/LIVE environment. This helper does not introduce event types, payment authority, subscription APIs or delivery guarantees.
