splitforms.com
← Back to the journal

Webhook Integrations for Web Forms: Slack, Zapier & More (2026)

Send web form submissions anywhere with webhooks — Slack, Zapier, Make, n8n, Discord. HMAC signing, retries, and no-code vs code setups explained with…

Start free — 500 submissionsSee pricing →No card, no time limit. Pro is $5/mo.
Short answer
A form webhook is an HTTP POST that fires the instant someone submits your form, pushing the data straight to Slack, Discord, a CRM, or your own server — real-time, no polling. Point your form at a destination's webhook URL and you're done. With splitforms you paste one HTTPS URL into the dashboard; it auto-detects Slack and Discord, signs every request with HMAC-SHA256 (an X-Splitforms-Signature header prefixed sha256=), and delivers each submission with an 8-second timeout and one automatic retry after about 2 seconds on network errors, timeouts and 5xx responses (not on 4xx) — so your endpoint should still acknowledge fast.
Try it on your own form

Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.

Create free account

What a form webhook is (and why you want one)

A webhook is the inverse of an API call. With a normal API you ask the server "anything new?" on a schedule and hope you didn't miss anything between polls. A webhook flips that around: the moment a visitor submits your form, the form backend calls you — an HTTP POST lands on a URL you chose, carrying the submission as JSON.

That real-time push is the whole point. There's no polling loop to run, no cron job, no delay. A lead fills out your contact form and a Slack message appears in #sales a few hundred milliseconds later. The same event can create a CRM record, append a row to a database, kick off an onboarding email, and ping Discord — all fired from one submission.

This post covers the how-to: destinations, receiver code, and signature verification. If you just want the dashboard setup, the form to webhook page is the shorter click-by-click version.

Concretely, a form webhook is good for:

  • Instant alerts — Slack or Discord messages the second a form is submitted, so nobody refreshes an inbox.
  • CRM & database sync — push leads into HubSpot, Airtable, Postgres, or Notion without a middleman.
  • Automation — trigger Zapier, Make, or n8n workflows that branch on the submission data.
  • Custom logic — run your own scoring, routing, or enrichment on your own server.

The payload shape is stable and predictable. Here's what a splitforms submission event looks like on the wire:

POST https://your-handler.example.com/api/webhook
Content-Type: application/json
X-Splitforms-Signature: sha256=8c7c0b2e9d41...
X-Splitforms-Event: submission.created
User-Agent: splitforms.com-webhook/1.0

{
  "event": "submission.created",
  "submission": {
    "id": "9f2b7c3a-4e2d-4c1a-9d0e-1a2b3c4d5e6f",
    "form_name": "Contact form",
    "data": {
      "name": "Ada Lovelace",
      "email": "[email protected]",
      "message": "Hello, world."
    },
    "ip_address": "203.0.113.42",
    "referer": "https://example.com/contact",
    "created_at": "2026-07-09T14:00:00.000Z"
  }
}

Two things to note before you write a line of receiver code: your form's fields arrive nested under submission.data, not at the top level; and the X-Splitforms-Signature header is the literal prefix sha256= followed by the hex HMAC-SHA256 of the raw request body. Verifying it is covered in the code section below.

Where you can send form data

Almost every modern service accepts an inbound webhook. The setup differs slightly per destination, but the pattern is identical: the destination hands you a URL, you paste it into your form backend, submissions start flowing. Here's the practical map.

DestinationSetupBest for
SlackPaste an Incoming Webhook URLTeam notifications in a channel
DiscordPaste a channel Webhook URLCommunities & indie projects
ZapierCatch Hook trigger → 6,000+ appsNo-code fan-out, non-technical teams
MakeCustom webhook module → scenarioVisual multi-step automation
n8nWebhook trigger node (self-host or cloud)Self-hosted, code-friendly workflows
PipedreamHTTP source → Node/Python stepsDevelopers who want inline code
Your own endpointAny HTTPS route + shared HMAC secretFull control, custom business logic

Slack and Discord both give you a plain HTTPS webhook URL from their settings UI — no code, no OAuth. Zapier, Make, n8n, and Pipedream each expose a "catch this webhook" trigger that becomes the entry point of a larger workflow. And any HTTPS route you write yourself is a valid destination as long as it can verify a signature and return a 2xx.

The friction is that each destination expects a different JSON shape — Slack wants blocks, Discord wants embeds, your CRM wants its own field names. This is where a form backend earns its keep. With splitforms you paste a single HTTPS URL into the integrations dashboard; it recognises Slack, Discord, Microsoft Teams, Telegram, and WhatsApp URLs and formats the message natively for each platform. For everything else it sends the canonical signed JSON payload shown above, and you translate it once on your side.

The code: form in, webhook out

Two pieces of code cover the whole path. First, the form that collects the submission. Second, the endpoint that receives the webhook and verifies it. You only need the second piece if the destination is your own server — Slack, Discord, and the automation tools receive the webhook for you.

1. The HTML form

This is the entire front end. A plain HTML form that POSTs to splitforms, with your access key in a hidden field. No JavaScript, no fetch, no client library:

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />

  <label>
    Name
    <input type="text" name="name" required />
  </label>

  <label>
    Email
    <input type="email" name="email" required />
  </label>

  <label>
    Message
    <textarea name="message" rows="5" required></textarea>
  </label>

  <!-- Honeypot: humans don't see it, bots fill it -->
  <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />

  <button type="submit">Send</button>
</form>

When someone submits this, splitforms stores the submission, emails you, and — because you configured a webhook — POSTs the signed JSON payload to every destination URL you added. The access_key authenticates the form; the webhook URLs live in your dashboard, not in the HTML, so they're never exposed to the browser.

2. The receiving endpoint (Next.js route handler)

If your destination is your own server, you must verify that each request genuinely came from splitforms and not from an attacker who guessed your URL. The rule is absolute: read the raw request body, verify the HMAC-SHA256 signature with a constant-time compare, and return a 2xx before the 8-second timeout. Here's a complete App Router route handler:

import crypto from "node:crypto";

const SECRET = process.env.SPLITFORMS_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  // 1. Read the RAW body. Never JSON.parse before you have verified it —
  //    re-serialising the parsed object changes the bytes and breaks the HMAC.
  const raw = await req.text();

  // 2. Recompute the signature. Header format: sha256=<hex hmac of raw body>
  const provided = req.headers.get("x-splitforms-signature") ?? "";
  const expected =
    "sha256=" + crypto.createHmac("sha256", SECRET).update(raw).digest("hex");

  // 3. Compare in constant time so the signature can't be leaked
  //    byte-by-byte — and guard the lengths first, since
  //    timingSafeEqual throws on a length mismatch.
  const a = Buffer.from(provided);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return new Response("bad signature", { status: 401 });
  }

  // 4. Acknowledge FAST — splitforms aborts after 8 seconds and retries
  //    only once, so heavy work goes off the request path.
  const payload = JSON.parse(raw);
  queueMicrotask(() => handleSubmission(payload.submission));

  return new Response("ok", { status: 200 });
}

Three details make or break this handler:

  1. Use the raw body for the HMAC — if a JSON middleware parses it first, the re-serialised bytes won't match the signature.
  2. Compare against the full sha256=-prefixed header with crypto.timingSafeEqual, never ===. The two most common verification bugs are comparing a bare hex digest against the prefixed header, and using a string equality that can be attacked one byte at a time.
  3. Return a 2xx quickly — splitforms aborts the request after 8 seconds and retries a timeout only once, so a consistently slow handler loses the delivery.

3. The same receiver in Express

Not on Next.js? The pattern ports to any framework. The one trap in Express is that express.json() parses the body before you can hash it — mount express.raw() on the webhook route instead:

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

const app = express();
const SECRET = process.env.SPLITFORMS_WEBHOOK_SECRET!;

// Capture the raw body for signature verification — express.json()
// would re-serialise the bytes and break the HMAC.
app.post(
  "/hooks/splitforms",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const provided = req.header("X-Splitforms-Signature") ?? "";
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");

    const a = Buffer.from(provided);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send("bad signature");
    }

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

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

app.listen(3000);

While developing, a tunnel gets the webhook to your laptop — point ngrok or a Cloudflare Tunnel at localhost:3000 and paste the public URL into the dashboard (splitforms can't reach 127.0.0.1 directly). You can inspect and sanity-check payload shapes with the webhook tester, and the signature format and event types are documented in the webhook docs.

No-code vs code: which path is yours

There are two honest ways to consume a form webhook, and the right one depends on whether you want to write and host code.

No-code: Zapier or Make

Point the webhook at a Zapier Catch Hook or a Make custom webhook module and build the rest of the flow by dragging boxes. The tool receives the POST, parses the JSON, and lets you map fields into 6,000+ apps — add a Google Sheets row, create a Trello card, send a Gmail, all without a server. This is the right choice when a non-technical teammate owns the workflow, when the logic changes often, or when you're wiring up apps that don't expose their own webhook endpoints.

The trade-offs: every step adds latency (a Zap can take seconds, not milliseconds), tasks are metered and billed, and you're trusting a third party with the payload. For most teams those costs are worth the zero-maintenance convenience.

Code: a signed webhook to your own endpoint

Point the webhook at an HTTPS route you wrote — the Next.js handler above — and you own the whole pipeline. It's the fastest path (single hop, no intermediary), the cheapest at volume (no per-task billing), and the most flexible (any language, any database, any business rule). The cost is that you are now responsible for verifying signatures and keeping the endpoint up — and since splitforms delivers each webhook exactly once, downtime means catching up from the dashboard rather than waiting for a retry.

A common hybrid: send the webhook to n8n or Pipedream, which gives you a code step and a hosted runtime, so you write the logic but skip the infrastructure. That's the sweet spot for developers who want control without running a server.

Worked example: routing leads with n8n

In n8n, drop a Webhook trigger node, set its HTTP method to POST, and paste the node's production URL into your form's webhook settings in splitforms. n8n receives the signed JSON payload and can branch into Postgres, Notion, HubSpot, Slack, or 400+ other nodes. A typical lead-routing flow:

Webhook (POST /splitforms-leads)
  ↓
IF { $json.body.submission.data.team_size === "250+" }
  → Slack: post to #sales-large
  → HubSpot: create Deal
ELSE
  → Slack: post to #sales-smb
  → Postgres: INSERT INTO leads ...

Note the field path: your form fields live under submission.data, not at the top level of the payload. This setup also answers the durability question below — n8n acknowledges splitforms' webhook delivery instantly, then its own error handling and retry settings deal with flaky downstream APIs.

Reliability: eight-second timeout and one retry

Signed with HMAC-SHA256 (sha256= signature header); 8-second timeout; one automatic retry after about 2 seconds on network errors, timeouts and 5xx responses (4xx is not retried); delivery log in the dashboard. Paid plans (Pro and Annual). A 2xx response counts as delivered. A 4xx response is not retried; network errors, timeouts and 5xx responses get one retry after about two seconds. There is no durable backoff queue or dead-letter queue. Manual dashboard and MCP tests make a single attempt.

What a failed delivery costs you (and what it doesn't)

A missed webhook is not a lost submission. The submission is stored in your dashboard and emailed to you regardless of what happens to the webhook, so the webhook is a routing layer, not the system of record. If your endpoint was down for an hour, every lead from that hour is still sitting in the dashboard — re-process from there, or pull them with the read API or a CSV export (paid plans).

Design for fast, idempotent delivery

  • Acknowledge first, work later. Return 200 immediately and queue the CRM write, email send, or LLM call — a slow downstream must never push you past the 8-second abort, because repeated timeouts can exhaust the retry.
  • Keep the endpoint boring. No auth walls (splitforms can't pass cookies or basic auth — the HMAC signature is your authentication), no redirects, no cold-start-prone infrastructure on the hot path.
  • Need durable retries? Queue downstream. Point the webhook at a receiver whose only job is to enqueue and return 200 — n8n, Pipedream, or a queue-backed endpoint — and let that layer retry the real work. The design trade-offs are covered in webhook retry strategy.

Troubleshooting failed deliveries

Four failure modes account for nearly every "my webhook doesn't work":

  • Signature mismatch on every event — you're hashing a parsed-and-re-serialised body instead of the raw bytes, or comparing a bare hex digest against the header, which is sha256=-prefixed.
  • Deliveries fail with a timeout — your endpoint took longer than 8 seconds to respond. Move heavy work off the request path and return 2xx immediately.
  • Nothing ever arrives — the URL points at localhost (use ngrok or a Cloudflare Tunnel) or sits behind authentication splitforms can't satisfy.
  • Fields look missing — in the generic payload, form fields are nested under submission.data, not at the top level.

FAQ

How do I send form data to Slack?

Create a Slack Incoming Webhook (Slack apps → Incoming Webhooks → Add to a channel), copy the https://hooks.slack.com/... URL, and paste it into your form backend. With splitforms you paste that single URL into the dashboard and it auto-detects Slack, formatting each submission as a tidy message with the name, email, and message fields — no payload templating required.

Do I need Zapier to send form submissions to a webhook?

No. A webhook is just an HTTP POST, so any tool that accepts one works — Slack and Discord accept webhooks natively, and your own server can receive them directly. Zapier, Make, n8n, and Pipedream are useful when you want to branch, transform, or fan out to many apps without writing code, but for a straight form-to-endpoint delivery they only add latency and cost.

How do I verify a webhook is genuine and not spoofed?

Check the X-Splitforms-Signature header before trusting the payload. It's the literal prefix sha256= followed by the hex HMAC-SHA256 of the raw request body, computed with your webhook's signing secret. On your server, recompute the same HMAC over the raw bytes — before any JSON parsing — and compare it to the full header value with crypto.timingSafeEqual (constant-time, not ==). Reject anything that doesn't match.

What happens if my webhook endpoint is down?

That delivery may fail. Signed with HMAC-SHA256 (sha256= signature header); 8-second timeout; one automatic retry after about 2 seconds on network errors, timeouts and 5xx responses (4xx is not retried); delivery log in the dashboard. Paid plans (Pro and Annual). There's no backoff sequence or dead-letter queue beyond that one retry. The webhook's row in your dashboard records the last HTTP status and last error so you can see what happened, and the submission itself is still stored and emailed to you, so no data is lost. If you need durable delivery, point the webhook at a receiver that acknowledges instantly and queues the work — n8n, Pipedream, or a small queue-backed endpoint of your own.

Can I send one form submission to multiple destinations?

Yes. Add several webhook URLs to the same form and each fires independently with its own delivery attempt (and its own retry on network errors, timeouts or 5xx) — a slow or failing Discord webhook never blocks your Slack or CRM delivery. For heavy fan-out (five or more apps, conditional routing, data transforms) point one webhook at n8n or Make and branch from there instead of managing many URLs by hand.

Next steps

Related articles

More practical guidance from integrations.

Browse the journal →
Integrations

How to Send Form Submissions to Salesforce (No Zapier, 2026)

Route HTML form submissions into Salesforce as Leads without Zapier — own your form HTML, de…

10 min readRead →
Integrations

How to Send Form Submissions to Pipedrive (No Zapier, 2026)

Route HTML form submissions into Pipedrive as Leads or Deals without Zapier — keep your own…

10 min readRead →
Integrations

How to Send Form Submissions to Kit (ConvertKit) in 2026

Add form subscribers to Kit (formerly ConvertKit) without Zapier — own your form HTML and de…

9 min readRead →

Explore this topic

Start with the overview, then move into focused guides.

OverviewWhat Is a Form Backend? (Complete Guide)Start here →GuideSelf-Hosted vs SaaS Form BackendRead →GuideForm Backend vs Form BuilderRead →GuideWhere Do Form Submissions Go?Read →ReferenceForm backend guideOpen →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 500 free submissions — no card, no time limit.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe