Skip to main content

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.

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:
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).