send.webforms.toRead the docs →

Developer endpoints

Live

Send form submissions to your own API

Sometimes the destination is your own backend, and everything you need is a reliable, verifiable POST. Webhook delivery sends each submission as JSON to a URL you control, signed with your secret so you can prove it came from us, and retried automatically when your endpoint is having a bad day. Webhook destinations are on Pro and Agency; Free sends email only.

Get an endpointRead the docs

What lands in Webhooks

Setup

  1. Add a webhook destination

    Point it at an HTTPS URL you control and copy the signing secret the dashboard generates.

  2. Verify the signature

    Compute HMAC-SHA256 over the raw request body using your secret and compare it to the signature header with a constant-time comparison.

  3. Return 2xx quickly

    Acknowledge fast and do slow work asynchronously. Non-2xx responses and timeouts are retried with backoff.

Webhooks example

Swap in your own endpoint and these run as-is.

What arrives at your endpoint.

payload.json
{
  "id": "sub_2f9x...",
  "formId": "frm_8a1c...",
  "createdAt": "2026-03-04T10:12:44.019Z",
  "data": {
    "name": "Ada Lovelace",
    "email": "ada@example.com",
    "message": "Do you support file uploads?"
  },
  "files": [],
  "meta": {
    "ip": "203.0.113.42",
    "country": "GB",
    "referrer": "https://example.com/pricing",
    "userAgent": "Mozilla/5.0 ..."
  },
  "spam": { "score": 0.02, "quarantined": false }
}

Verify the signature before trusting the body. Use the raw bytes, not a re-serialised object.

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, headers: Headers): boolean {
  const signature = headers.get("x-webforms-signature") ?? "";
  const timestamp = headers.get("x-webforms-timestamp") ?? "";

  // Reject anything older than five minutes to blunt replay attacks.
  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;

  const expected = createHmac("sha256", process.env.WEBFORMS_SECRET!)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}

Worth knowing

Webhooks form FAQ

Compute HMAC-SHA256 over timestamp.rawBody using your signing secret and compare the hex digest to the x-webforms-signature header using a constant-time comparison.

Sending from your stack

The destination is set in the dashboard, so the form itself is the same plain POST everywhere. These guides have the markup for the common setups.

Send your next form submission to Webhooks

Free for 300 submissions a month. Endpoint in under a minute, no card required.

Start free