formbuild.io

Webhook signing and delivery

How to verify webhook signatures, the payload format, retry behavior, and the delivery log.

Outbound webhooks are persisted, signed, and retried with exponential backoff. This page explains the wire format and how to verify the signature on your receiving server.

Payload format

Every submission triggers a POST with the following JSON body. Content type is application/json.

{
  "event": "form.submission",
  "formName": "Contact",
  "submittedAt": "2026-05-30T12:34:56.789Z",
  "data": {
    "name": "Alice",
    "email": "alice@example.com",
    "message": "Hi there"
  }
}

The data object contains exactly the fields the submitter filled in — keyed by the field name attribute, not the visible label.

Headers we send

HeaderValue
Content-Typeapplication/json
User-Agentformbuild.io/1.0

X-Formbuild-Signature

HMAC-SHA256 of the raw body, hex-encoded — only sent when a signing secret is configured

Setting up a signing secret

In your form's settings, on the Integrations tab, expand the Webhooks panel. You'll see a Signing secret control with a Generate button. Click it and copy the 64-character hex string — this is the last time you'll see the full value, so paste it into your server config now.

Verifying the signature on your server

The signature is computed over the raw request body as received — before any JSON parsing or whitespace normalization. Always read the body as bytes/string and compute HMAC-SHA256 with your secret, then compare against the header.

Node.js (Express)

import express from "express";
import crypto from "crypto";

const app = express();
const SECRET = process.env.FORMBUILD_WEBHOOK_SECRET;

// Capture the raw body so we can hash it byte-for-byte.
app.use(express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
  const signature = req.headers["x-formbuild-signature"];
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(req.body)
    .digest("hex");

  if (
    !signature ||
    !crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
    )
  ) {
    return res.status(401).send("Invalid signature");
  }

  // Signature is valid, process webhook
  console.log("Webhook received:", req.body);
  res.status(200).send("OK");
});

On this page