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

# Webhooks

> Verify signed webhook deliveries from Formitto.

Formitto delivers events to the webhook URLs you configure on your forms,
calendars, and shop widgets. Every delivery is **signed** so you can verify it
genuinely came from Formitto and wasn't tampered with or replayed.

## The signature header

Each delivery carries an `X-Formitto-Signature` header:

```
X-Formitto-Signature: t=1748550000,v1=5d41402abc4b2a76b9719d911017c592...
```

* `t` — the Unix timestamp (seconds) when the delivery was signed.
* `v1` — the HMAC-SHA256 signature, hex-encoded.

The signature is computed over the string `` `${t}.${rawBody}` `` using your
account's **webhook signing secret** (found in the dashboard under
**Settings → Webhooks**), with **HMAC-SHA256**.

## Verifying a delivery

Always verify against the **raw request body bytes** — don't parse and
re-serialize the JSON first, or the signature won't match. The SDK bundles a
verifier:

```typescript theme={null}
import { webhooks } from "@formitto/sdk";

// In your webhook handler — `rawBody` is the exact bytes received.
const ok = webhooks.verifySignature({
  payload: rawBody,
  signatureHeader: req.headers["x-formitto-signature"],
  secret: process.env.FORMITTO_WEBHOOK_SECRET,
});

if (!ok) {
  return res.status(400).send("invalid signature");
}
// Safe to trust + process the event.
```

`verifySignature` returns `true` only when the HMAC matches **and** the
timestamp is within the replay window. It never throws — malformed input
returns `false`.

## Replay protection

The timestamp is part of the signed material, so a captured delivery replayed
later fails verification once it falls outside the tolerance window —
**300 seconds** (5 minutes) by default. Override it if your processing needs a
different window:

```typescript theme={null}
webhooks.verifySignature({ payload, signatureHeader, secret, tolerance: 600 });
```

## Verifying without the SDK

The scheme is Stripe-style, so it's easy to reproduce in any language. In
Node.js:

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

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string]),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(parts.v1, "utf8");
  const b = Buffer.from(expected, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

## Event payloads

A form submission delivers a `form.submitted` event:

```json theme={null}
{
  "event": "form.submitted",
  "wc_client_current": null,
  "additional_fields": {
    "form_id": 123,
    "form_name": "Contact form",
    "Name": "Jane Doe",
    "Email": "jane@example.com"
  }
}
```

Booking and order events follow the same signing scheme — verify them
identically. Failed deliveries are retried with exponential backoff, so make
your handler **idempotent** (dedupe on the event's resource id).
