splitforms.com
All articles/ INTEGRATIONS6 MIN READPublished May 1, 2026

Send form data to a webhook in 3 steps

Wire any HTML form to a webhook endpoint (Slack, Discord, n8n, your own server) in under 5 minutes. Working examples with retries.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

What a form webhook is

A webhook is the inverse of an API call. Instead of you polling for new submissions, the form backend calls you whenever a submission arrives. The receiving server gets an HTTP POST with a JSON body, processes it, and returns a 2xx status to acknowledge.

The shape of the splitforms webhook payload is stable:

POST https://your-handler.example.com/hooks/form
Content-Type: application/json
X-Splitforms-Delivery: 01J2K8XQVM4FH5G6JQ7Y8WN9ZA
X-Splitforms-Signature: t=1714579200,v1=8c7c0b...
User-Agent: splitforms-webhooks/1

{
  "form_id": "frm_5fK9...",
  "submitted_at": "2026-05-01T14:00:00.000Z",
  "ip": "203.0.113.42",
  "fields": {
    "name": "Ada Lovelace",
    "email": "ada@example.com",
    "message": "Hello, world."
  },
  "spam_score": 0.02
}

Step 1 — Get a webhook URL

Every destination gives you a long opaque URL. Treat it like a password: anyone who has it can post to your channel, your database, or your automation. A few common sources:

  • Slack — Apps → Custom Integrations → Incoming Webhooks → Add Configuration. Pick the channel, copy the URL.
  • Discord — Server Settings → Integrations → Webhooks → New Webhook → Copy URL.
  • n8n / Make / Zapier — Add a "Webhook" trigger node, copy the production URL.
  • Your own server — Any HTTPS endpoint that accepts POST with JSON. We'll wire one up below.

Step 2 — Connect it to splitforms

In your splitforms dashboard open the form, click Webhooks, paste the URL, and save. There's a "Send test event" button that fires a fake submission so you can confirm wiring before going live.

If you're wiring it programmatically (or want it in source control), the API does the same:

curl -X POST https://splitforms.com/api/forms/frm_5fK9.../webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.slack.com/services/T0/B0/XXXX",
    "events": ["submission.created"],
    "secret": "whsec_at_least_32_chars_long_random_string"
  }'

Step 3 — Verify and dedupe

Anyone on the internet who learns your handler URL can hit it. To trust the payload you need two checks:

  1. Verify the HMAC signature using the shared secret you set when creating the webhook.
  2. Deduplicate by X-Splitforms-Delivery — retries reuse the ID, so you store IDs you've seen and ignore repeats.

Code for both is in the custom server section.

Slack example

splitforms ships a Slack-shaped formatter. Pick "Slack" as the integration type and your form fields render as a tidy attachment with name, email, and message. If you'd rather control the format yourself, send to a generic webhook with a JSON template:

{
  "text": "New form submission",
  "blocks": [
    { "type": "header", "text": { "type": "plain_text", "text": "{{ fields.name }} contacted you" } },
    { "type": "section", "fields": [
      { "type": "mrkdwn", "text": "*Email:*\n{{ fields.email }}" },
      { "type": "mrkdwn", "text": "*Spam score:*\n{{ spam_score }}" }
    ] },
    { "type": "section", "text": { "type": "mrkdwn", "text": "{{ fields.message }}" } }
  ]
}

Discord example

Discord webhooks accept a similar JSON shape. The embedsarray is the closest analogue to Slack's blocks:

{
  "username": "splitforms",
  "embeds": [{
    "title": "New submission to {{ form_name }}",
    "description": "{{ fields.message }}",
    "color": 5814783,
    "fields": [
      { "name": "Name", "value": "{{ fields.name }}", "inline": true },
      { "name": "Email", "value": "{{ fields.email }}", "inline": true }
    ],
    "timestamp": "{{ submitted_at }}"
  }]
}

n8n example

In n8n, drop a Webhook trigger node, set the HTTP method to POST, and copy the production URL into splitforms. Downstream you can branch into Postgres, Notion, OpenAI, or 400+ other nodes. A typical lead-routing flow:

Webhook (POST /splitforms-leads)
  ↓
IF { $json.fields.company_size > 50 }
  → Slack: post to #sales-large
  → HubSpot: create Deal
ELSE
  → Slack: post to #sales-smb
  → Postgres: INSERT INTO leads ...

Latency from form submit to Slack message in this setup is typically 400–700ms — splitforms accounts for ~200ms of that, n8n the rest.

Custom server with HMAC verification

For your own backend the verification pattern is the same regardless of language. Node.js with Express:

import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET = process.env.SPLITFORMS_WEBHOOK_SECRET!;
const seen = new Set<string>(); // use Redis in production

// Capture raw body for signature verification
app.post(
  "/hooks/splitforms",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sigHeader = req.header("X-Splitforms-Signature") ?? "";
    const deliveryId = req.header("X-Splitforms-Delivery") ?? "";

    // Header format: t=<unix_ts>,v1=<hex_hmac>
    const parts = Object.fromEntries(
      sigHeader.split(",").map((kv) => kv.split("=") as [string, string]),
    );
    const signed = `${parts.t}.${req.body.toString("utf8")}`;
    const expected = crypto
      .createHmac("sha256", SECRET)
      .update(signed)
      .digest("hex");

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

    // Idempotency
    if (seen.has(deliveryId)) return res.status(200).send("duplicate");
    seen.add(deliveryId);

    const payload = JSON.parse(req.body.toString("utf8"));
    // ... do your thing with payload.fields ...

    res.status(200).send("ok");
  },
);

app.listen(3000);

Three things to remember: use the raw body for HMAC (parsing with express.json() first will mangle the bytes), use timingSafeEqual to avoid timing attacks, and reject signatures whose timestamp is more than 5 minutes old to prevent replay.

Retries & idempotency

splitforms considers any 2xx response successful. Anything else triggers retries with exponential backoff: 1s, 5s, 30s, 2 minutes, 10 minutes, 1 hour, and then hourly until 24 hours have elapsed. After that the delivery is marked failed and you get an email digest.

AttemptDelay from previousCumulative time
10s0s
21s1s
35s6s
430s36s
52m2m 36s
610m12m 36s
71h1h 12m
8–N1h eachup to 24h

Because retries reuse the X-Splitforms-Delivery header, your handler should be idempotent. Store seen IDs in Redis with a 48-hour TTL and short-circuit duplicates.

Testing webhooks locally

The annoying part of webhook development is that the webhook source needs to reach your laptop, which is usually behind NAT. Three patterns I use:

  • ngrok or Cloudflare Tunnel — start a tunnel pointing at localhost:3000, paste the public URL into splitforms' webhook config, watch live traffic. Cloudflare Tunnel is free and doesn't change URLs across restarts if you create a named tunnel.
  • webhook.site — a free public dropbox for inspecting payloads without writing any code. Useful for "what does the JSON actually look like" debugging.
  • splitforms test event button — sends a known-good payload to your URL on demand, including a real signature you can verify against. Use this to test your HMAC code before going live.

Whatever you pick, log every inbound webhook for the first week with full headers and body. Once you're confident, narrow the logs to error cases only — but the early-day inspection cost is the cheapest debugging you'll ever do.

Security checklist

Before pointing a production form at any webhook destination:

  1. HTTPS only. splitforms refuses to deliver to plain HTTP URLs. If yours appears to allow it, you're looking at a man-in-the-middle waiting to happen.
  2. Verify HMAC every time. Reject anything without a valid signature. Don't make exceptions for "test" traffic.
  3. Reject stale signatures. Treat anything older than 5 minutes as a replay attempt.
  4. Idempotency by delivery ID. Use a Redis SET with a 48-hour TTL keyed on the delivery header.
  5. Allowlist outbound IPs if you're paranoid. splitforms publishes its egress IP ranges on a status page so your firewall can lock down the receiving endpoint to known sources.
  6. Don't log payload secrets. If your form collects a token or password, scrub it before any structured log call.

Tech support and troubleshooting

Five webhook errors that account for almost every "why doesn't this work":

  • Signature mismatch on every eventYou're hashing the parsed body, not the raw bytes. Capture the request body before any JSON middleware runs and HMAC the raw buffer.
  • Duplicate rows in your databaseHandler isn't idempotent. Use the X-Splitforms-Delivery ID as the dedupe key and SETEX it in Redis for 48 hours.
  • Endpoint times out under loadsplitforms expects 2xx within ~10 seconds. Acknowledge fast and queue heavy work to a background job; otherwise retries pile up.
  • Test events never arriveEither the URL is HTTP (rejected) or your firewall is blocking splitforms egress IPs. Check the status page for the published IP ranges.
  • Replay attack suspicionReject any signature whose timestamp is older than 5 minutes. Capture the timestamp from the X-Splitforms-Timestamp header and bound it.

The full webhook spec, signature format, and event types are in the splitforms docs and the API reference. For account questions see the splitforms FAQ.

FAQ

What is a form webhook?

A form webhook is an HTTP POST callback that fires every time a user submits your form. The form backend forwards the submission data — usually as JSON — to a URL you specify, so you can route it into Slack, a database, an automation tool, or your own API.

How do I send form data to Slack?

Create a Slack Incoming Webhook URL, then configure your form backend to POST a JSON payload with a `text` or `blocks` field to that URL. With splitforms you paste the URL into the dashboard and it formats the message automatically.

Should I verify the webhook signature?

Yes, for any webhook reaching your own server. Sign each request with HMAC-SHA256 using a shared secret and verify the `X-Splitforms-Signature` header before trusting the payload. Without verification an attacker who guesses your URL can post fake submissions.

What happens if my webhook endpoint is down?

splitforms retries failed deliveries with exponential backoff (1s, 5s, 30s, 2m, 10m, 1h) for up to 24 hours. Each delivery has a unique idempotency key in the `X-Splitforms-Delivery` header so you can safely deduplicate retries.

Is a webhook the same as Zapier?

No. A webhook is the raw HTTP callback. Zapier (and n8n, Make, etc.) consume webhooks and let you build no-code workflows on top of them. A webhook is faster and free; an automation tool is more flexible but adds latency and cost.

Next steps

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

1,000 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your inbox.

Generate access key →Read the docs
v0.1 · founders pricing locked in · early access open