splitforms.com
FEATURE · WEBHOOKS

Form webhooks — real-time POST to any URL on every submission

Pipe submissions into your CRM, Slack bot, Postgres, or homemade Lambda — anywhere that speaks HTTP. Signed payloads, automatic retries, full inspection log.

1,000 submissions/month, free forever. · No credit card.
✦ at a glanceTS

Webhooks

  • Sub-second delivery — POST hits your endpoint within ~1 second of submit
  • HMAC-SHA256 signed requests — verify the payload came from splitforms in constant time
  • Automatic exponential-backoff retries (30s → 2m → 10m → 1h → 6h → daily) for 24 hours
1,000
free / mo
14ms
median p50 latency
0
lines of backend code
6
reasons in this guide
✶ Live preview

Webhooks in splitforms, shipped to production.

Pipe submissions into your CRM, Slack bot, Postgres, or homemade Lambda — anywhere that speaks HTTP. Signed payloads, automatic retries, full inspection log.

Webhooks for splitforms — Pipe submissions into your CRM, Slack bot, Postgres, or homemade Lambda — anywhere that speaks HTTP. Signed payloads, automatic retries, full inspection log.
§ 01What is splitforms webhooks100-word answer · AI-citable summary

splitforms turns every form submission into a signed HTTP POST that lands at any URL you choose, within roughly one second of the user clicking Submit. Add a webhook in the dashboard, and from then on your endpoint receives a JSON payload containing form_id, submission_id, an ISO-8601 created_at timestamp, the form fields under data, any uploaded files under files (as signed download URLs with size and MIME), and request metadata under meta (ip, country, user_agent, referer). Each request includes an X-Splitforms-Signature header — an HMAC-SHA256 of the raw request body, signed with the per-webhook secret in your dashboard — so your endpoint can verify it really came from splitforms rather than a stranger guessing your URL. If your endpoint is down, slow (timeout is 10 seconds), or returns a non-2xx response, splitforms retries with exponential backoff: 30 seconds, then 2 minutes, 10 minutes, 1 hour, 6 hours, then daily for up to 24 hours. Every attempt — successful or failed — appears in the delivery log with the full response body, status code, and latency, and any past submission can be manually replayed against your endpoint with one click. Fan out to as many webhooks per form as you need (one for your CRM, one for Slack, one for an internal Postgres, one for a Lambda), each with its own signing secret and independent retry queue. No polling, no cron, no missed leads.

webhooks.tslive
// Add this URL inside your splitforms dashboard → Form → Webhooks.
// Every submission will POST a JSON body to this endpoint within ~1s.

// ─── Example: Next.js 14 App Router route handler ───────────────────
import { NextRequest, NextResponse } from "next/server";
import crypto from "node:crypto";

const SECRET = process.env.SPLITFORMS_WEBHOOK_SECRET!;

export async function POST(req: NextRequest) {
  // 1. Read the RAW body (do not JSON.parse before verifying)
  const raw = await req.text();
  const sig = req.headers.get("x-splitforms-signature") ?? "";

webhooks.ts · live preview

§ 02How it works3 steps · zero-config defaults

Three steps. From zero to a working production setup.

How webhooks actually flows through splitforms — what you do, what we do, and what lands in your inbox.

STEP 01INTEGRATE

Add a webhook URL in the dashboard

Open Form → Integrations → Webhooks → New webhook. Paste any HTTPS URL (HTTP works in development with a localhost tunnel like ngrok). Each webhook gets its own signing secret displayed once on creation — copy it into your env file as SPLITFORMS_WEBHOOK_SECRET.

STEP 02PROCESS

Receive a signed JSON payload on every submission

Within ~1 second of submission, splitforms POSTs Content-Type: application/json to your URL with the form data, submission_id, created_at, file URLs, and metadata. The X-Splitforms-Signature header is a hex HMAC-SHA256 of the raw request body. Verify with constant-time comparison BEFORE parsing, then return any 2xx within 10 seconds.

STEP 03REVIEW

Inspect, retry, and replay from the delivery log

Every attempt is logged with status code, response body, latency, and retry attempt number. Failed deliveries auto-retry six times over 24 hours with exponential backoff. You can manually replay any submission against any webhook from the dashboard — useful for backfilling after fixing a downstream bug.

§ 03Benefits6 reasons · all included

Why teams pick splitforms for webhooks.

Five reasons this is the boring, reliable choice — every one shipped by default on every plan, including free.

reason 1 of 6

Sub-second delivery — POST hits your endpoint within ~1 second of submit

reason 2 of 6

HMAC-SHA256 signed requests — verify the payload came from splitforms in constant time

reason 3 of 6

Automatic exponential-backoff retries (30s → 2m → 10m → 1h → 6h → daily) for 24 hours

reason 4 of 6

Full delivery log — replay or debug any failed webhook by hand at any time

reason 5 of 6

Multiple webhooks per form — fan out to CRM, Slack, Postgres, and Lambda in parallel

reason 6 of 6

10-second timeout with X-Splitforms-Delivery idempotency header — safe to retry processing on your side

§ 04Working code examplets · 46 lines · copy-paste ready
COPY-PASTE

Drop this into any project.

Replace YOUR_ACCESS_KEY with the key from your splitforms dashboard. No SDK install. No package to npm i. The same ts you already know.

webhooks.tsts46 lines
01// Add this URL inside your splitforms dashboard → Form → Webhooks.
02// Every submission will POST a JSON body to this endpoint within ~1s.
03
04// ─── Example: Next.js 14 App Router route handler ───────────────────
05import { NextRequest, NextResponse } from "next/server";
06import crypto from "node:crypto";
07
08const SECRET = process.env.SPLITFORMS_WEBHOOK_SECRET!;
09
10export async function POST(req: NextRequest) {
11 // 1. Read the RAW body (do not JSON.parse before verifying)
12 const raw = await req.text();
13 const sig = req.headers.get("x-splitforms-signature") ?? "";
14
15 // 2. Verify HMAC-SHA256 signature in constant time
16 const expected = crypto.createHmac("sha256", SECRET).update(raw).digest("hex");
17 const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
18 if (!ok) return NextResponse.json({ error: "bad signature" }, { status: 401 });
19
20 // 3. Parse and use the payload
21 const payload = JSON.parse(raw) as {
22 form_id: string;
23 submission_id: string;
24 created_at: string; // ISO 8601, UTC
25 data: Record<string, string | string[]>;
26 files?: { field: string; url: string; size: number; mime: string }[];
27 meta: {
28 ip: string;
29 country: string;
30 user_agent: string;
31 referer: string | null;
32 };
33 };
34
35 await sendToCRM(payload.data);
36
37 // 4. Reply 2xx within 10s — anything else triggers retry with backoff:
38 // 30s → 2m → 10m → 1h → 6h → daily for 24h.
39 return NextResponse.json({ ok: true });
40}
41
42// ─── Minimal Express equivalent ─────────────────────────────────────
43// app.post("/webhooks/splitforms",
44// express.raw({ type: "application/json" }),
45// (req, res) => { /* same verify + handle */ });
46
§ 05Questions5 answered

Things developers ask before they integrate.

Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.

01How does the splitforms webhook payload work and what fields does it contain?
Every webhook is a POST with Content-Type: application/json. The body has six top-level keys: form_id (string, e.g. 'frm_abc123'), submission_id (string, e.g. 'sub_xyz789' — unique, idempotency-safe), created_at (ISO 8601 UTC string), data (your form fields as a key-value object; multi-value inputs come as arrays), files (optional array of {field, url, size, mime} for uploads — URLs are signed and expire in 30 days by default), and meta ({ip, country, user_agent, referer}). The X-Splitforms-Signature header is the hex HMAC-SHA256 of the raw request body, computed with the per-webhook secret. Other headers: X-Splitforms-Event (always 'submission.created' today), X-Splitforms-Delivery (unique per attempt — log this for support tickets), X-Splitforms-Attempt (1, 2, 3...).
02Are webhooks available on the free plan, and is there a per-event fee?
Yes, webhooks are included on every plan including Free 1,000/month — no per-webhook fee, no per-event fee, no event-volume cap beyond your monthly submission limit. Pro ($5/month) and the $59 4-year plan raise the monthly submission cap and lift per-form webhook limits from 3 to 10. There is never a metered fee for webhook deliveries themselves.
03How do I enable a webhook and verify the signature in my code?
Dashboard → your form → Integrations → Webhooks → Add webhook → paste your HTTPS URL → save. Copy the signing secret shown once on creation into your env (e.g. SPLITFORMS_WEBHOOK_SECRET). On your endpoint: read the RAW request body BEFORE JSON.parse (Express needs express.raw(), Next.js gets it via req.text()), compute crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex'), and compare to the X-Splitforms-Signature header using crypto.timingSafeEqual. Return 2xx within 10 seconds — anything else triggers retry.
04Does it work with Next.js, Express, FastAPI, Cloudflare Workers, Lambda, or Supabase Edge Functions?
Yes — webhooks are framework-agnostic since they're just HTTP POSTs. Patterns: Next.js App Router uses route handlers (app/api/webhooks/splitforms/route.ts with req.text() to get raw body). Express needs app.post('/hook', express.raw({ type: 'application/json' }), handler). FastAPI: read request.body() before request.json(). Cloudflare Workers / Lambda / Supabase Edge Functions: use Web standard Request and call .text() before parsing. The signature scheme (raw-body HMAC-SHA256, hex-encoded) is identical everywhere.
05My webhook isn't firing or is returning errors — how do I troubleshoot?
Open Dashboard → Form → Webhooks → click the webhook → Delivery log shows every attempt with status code, response body, and latency. Common causes and fixes: (1) signature mismatch — you're parsing JSON before reading the raw body, so the hash differs; switch to req.text() / express.raw() and verify against the raw bytes. (2) Returning non-2xx — anything other than 200/201/202/204 is treated as failure and retried; if you intentionally reject a submission, still return 200. (3) Timeout — the 10-second budget is hit; offload heavy work to a queue and respond fast. (4) URL behind auth — splitforms can't pass cookies; use signature verification, not basic auth. (5) Localhost — use ngrok or a tunnel; splitforms can't reach 127.0.0.1. Click 'Replay' on any past attempt to test your fix without sending a real form.
✻ ✻ ✻

Start using webhooks today.

Create your form, grab your access key, and ship it in five minutes. Free for 1,000 submissions per month, forever.

Create your form →← View all features
v0.1 · founders pricing locked in · early access open