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.
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.
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:
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:
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:
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:
{
"event": "form.submitted",
"wc_client_current": null,
"additional_fields": {
"form_id": 123,
"form_name": "Contact form",
"Name": "Jane Doe",
"Email": "[email protected]"
}
}
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).