Every lead form is a promise about how fast you will reply. A Slack channel turns each submission into something a person can claim and answer in minutes.
A lead form is only as good as the speed of the reply behind it. The form itself is the easy part. The gap that costs pipeline is the one between someone submitting it and a human actually seeing it, which on most sites is a notification email sitting in a shared inbox nobody has open.
Putting that submission into a Slack channel closes the gap, and it changes the shape of the follow-up: a lead becomes a message someone can claim, reply to in a thread, and mark as handled, rather than an email five people assume someone else has answered.
This guide builds it two ways. The first uses Zapier and needs no code. The second receives the webhook in your own Route Handler, which costs a little code and buys signature verification, custom routing, and no per-task billing.
What do you need to send Webflow leads into Slack?
A published Webflow form, a Slack workspace you can install an app into, and then either a Zapier account or a Webflow Cloud project depending on which route you take. Nothing here requires a paid Webflow plan to begin.
The full list before you start:
- A published Webflow site with at least one form. Test on the published site rather than the Designer canvas, since Webflow receives submissions on the webflow.io staging subdomain and on a custom domain with an active Site plan
- A Slack workspace where you can install an app, which usually needs an admin
- For the no-code route, a Zapier account. Check current task pricing on Zapier's pricing page, since the plan that fits depends on your submission volume
- For the coded route, a Webflow Cloud project running Next.js, plus Node.js 22.13.0 or higher locally, the floor set by Webflow CLI 2.x. Webflow Cloud is available from the free Starter site plan up, though mounting the app to a custom domain requires Premium or higher
Once these are in place, the decision that shapes everything else is whether a hosted platform relays the submission or you receive it yourself. Here's how.
How a Webflow lead notification reaches Slack
Whichever route you pick, the same relay runs underneath: a form submission raises a Webflow webhook, something receives it, reshapes it, and posts it into a channel.
The stage that decides the quality of the result is data transformation. A raw form payload dumped into Slack is unreadable, and an unreadable notification gets muted within a week, so knowing the payload shape is what lets you format it well.
What Webflow actually sends
Before you map anything, look at what arrives.
A form submission event arrives in this shape, abridged here since the full payload also carries schema, formElementId and localeId:
{
"triggerType": "form_submission",
"payload": {
"name": "Contact Us",
"siteId": "65427cf400e02b306eaa049c",
"data": {
"First Name": "Zaphod",
"Last Name": "Beeblebrox",
"email": "zaphod@heartofgold.ai",
"Phone Number": 15550000000
},
"submittedAt": "2022-09-14T12:35:16.117Z",
"id": "6321ca84df3949bfc6752327",
"formId": "65429eadebe8a9f3a30f62d0"
}
}
Two details in there cause most of the mapping bugs I see. The keys inside data are the human-readable field names from the Designer, including capitalisation and spaces. It is "First Name", not first-name. Renaming a field in the Designer therefore breaks every downstream mapping that referenced the old name, so treat field names as an interface rather than a label.
Do not assume every value is a string either. Webflow documents no type coercion for form values, but the reference's own example shows "Phone Number" unquoted, so a numeric-looking field can reach you as a number. Read defensively and convert before formatting. Once you can predict the payload, the mapping steps below are straightforward.
5 steps to send Webflow form leads to Slack with Zapier
The no-code route is a single Zap: Webflow as an instant trigger, Slack as the action, and a filter once more than one form feeds it. The work that matters is deciding what deserves a notification and formatting the message so it stays readable.
1. Decide what deserves an interruption
Do this before you build anything. A channel that receives every submission from every form becomes noise, and muted notifications are worse than none because everyone assumes they are covered.
Pick the forms that represent real buying intent, such as a demo request or a contact-sales form, and leave newsletter signups out of it. If you only have one form, plan to route on a field value instead, which step 5 covers. You should finish this step with a written list of which forms notify and which do not.
2. Connect the Webflow trigger
In Zapier, create a Zap with Webflow as the trigger app and Form Submission as the event, then authorise your Webflow account and pick the site and form. Zapier registers the webhook with Webflow for you, which is why this is an instant trigger rather than a poll.
Submit a real test entry on the published site so Zapier has sample data to map against. This is the step people skip, and without it the field pickers in the next step are empty. A successful test shows your submitted values in Zapier's sample record.
3. Map the fields into a Slack message
Add Slack as the action app, choose Send Channel Message, and pick the channel. Now build the message body from the sample data rather than dropping the whole payload in.
A notification that works reads top to bottom in the order a human triages it: who it is, what they asked for, how to reach them, and which form it came from. Put the reply-to address on its own line so it is one tap to copy on mobile. When the mapping is right, the preview shows real values rather than field tokens.
4. Make the message worth reading
Slack renders *bold* in message text, so labelling each value costs nothing and makes the message scannable. The top-level text field is mrkdwn by default, which is why this works without extra configuration.
Send yourself a test message and read it on a phone before you consider this done, because that is where most people will first see it. The test should arrive in the channel formatted, not as a wall of unlabelled values.
5. Route by form or by value
Once more than one form feeds the Zap, add a filter step so enterprise enquiries and general contact requests land in different channels. Filtering on a field value, such as a company-size dropdown, is what keeps a shared channel useful as volume grows.
Turn the Zap on and submit one more real entry. A correctly filtered Zap either posts to the right channel or halts on the filter step, and Zapier's history shows you which happened.
The coded route: receive the Webflow webhook yourself
Zapier is the right answer when marketing owns the workflow and nobody wants to maintain code. It stops being right when you need the lead written somewhere else at the same time, when per-task cost starts to matter, or when you want to prove the request genuinely came from Webflow.
That last one is the real argument. With a hosted automation platform you never see the signature: verification happens inside the vendor, so you have no way to prove a given message came from Webflow rather than from anyone who reached the endpoint. Webflow signs every webhook so you can check it yourself, and the webhook documentation sets out the scheme. Requests carry an x-webflow-timestamp and an x-webflow-signature, the signature being a SHA-256 HMAC over the timestamp and the raw body joined by a colon, signed with your webhook's secret.
Add the endpoint under your form's settings: next to Send to, add a Webhook and enter your Route Handler URL. Webflow then shows a secret key exactly once, so copy it before dismissing the dialog, because it cannot be retrieved afterwards.
Webflow recommends using its SDK's own verification method, since the implementation may change and an SDK update carries you along. The handler below validates manually.
That is the pattern to reach for on the Workers runtime when you would rather not pull in the full SDK:
// app/api/lead-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
const FIVE_MINUTES = 300_000
function toBytes(hex: string): Uint8Array {
return new Uint8Array(
(hex.match(/.{1,2}/g) ?? []).map((byte) => parseInt(byte, 16))
)
}
async function isAuthentic(
secret: string,
timestamp: string,
rawBody: string,
signature: string
): Promise<boolean> {
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
// Webflow signs the timestamp and the raw body joined by a colon.
const mac = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(`${timestamp}:${rawBody}`)
)
const expected = new Uint8Array(mac)
const provided = toBytes(signature)
// Length must match before a constant-time compare is meaningful.
if (expected.length !== provided.length) return false
return crypto.subtle.timingSafeEqual(expected, provided)
}
export async function POST(request: NextRequest) {
const timestamp = request.headers.get('x-webflow-timestamp')
const signature = request.headers.get('x-webflow-signature')
if (!timestamp || !signature) {
return NextResponse.json({ error: 'Unsigned request' }, { status: 401 })
}
// Reject replays before doing any cryptographic work.
if (Date.now() - Number(timestamp) > FIVE_MINUTES) {
return NextResponse.json({ error: 'Stale request' }, { status: 401 })
}
// Read the body as text: the signature covers the exact bytes sent.
const rawBody = await request.text()
const secret = process.env.WEBFLOW_WEBHOOK_SECRET as string
if (!(await isAuthentic(secret, timestamp, rawBody, signature))) {
return NextResponse.json({ error: 'Bad signature' }, { status: 401 })
}
const event = JSON.parse(rawBody) as {
payload: { name: string; data: Record<string, string | number> }
}
const fields = event.payload.data
const lines = Object.entries(fields)
.map(([label, value]) => `*${label}:* ${String(value)}`)
.join('\n')
await fetch(process.env.SLACK_WEBHOOK_URL as string, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `New lead from *${event.payload.name}*\n${lines}`,
}),
})
// Answer Webflow quickly; it does not need your Slack result.
return NextResponse.json({ received: true })
}
Four things here are deliberate. The timestamp is checked before any hashing, because Webflow's guidance is to treat a request older than five minutes as a possible replay, and rejecting early means a flood of stale requests cannot make your Worker do cryptographic work.
The body is read with request.text() rather than request.json(). Webflow signs the body as it was sent, so hashing the unmodified text is the safe route: re-serialising a parsed object can change key order or whitespace and break the check.
The comparison uses timingSafeEqual after a length guard rather than === on two hex strings, because string comparison returns early on the first differing character, which leaks how much of a guess was correct. Note that timingSafeEqual is a Cloudflare extension rather than a standard Web Crypto method, so it exists on the deployed Workers runtime but not under a plain local next dev.
The handler awaits the Slack call before answering Webflow. That is fine at lead volumes, and it is the first thing to change if Slack ever gets slow, because senders retry on a non-2xx or a prolonged delay and that turns one lead into four notifications. Store both secrets as environment variables marked as secrets, then deploy and submit a test lead: a valid submission posts to Slack and returns a 200, and a forged request gets a 401.
What causes Webflow lead notifications to fail? Tips to troubleshoot
Four failures cover nearly everything: the edge runtime directive, a body that was parsed before it was hashed, a renamed Designer field, and a channel that has become noise.
The build fails after adding the Route Handler
Cause: an export const runtime = 'edge' directive in the route file. Webflow Cloud deploys Next.js through the OpenNext Cloudflare adapter, which does not support the Next.js edge runtime.
Fix: remove the line. Route Handlers run on the Workers runtime without it, and the next deploy succeeds.
Every signature check fails
Cause: almost always the body. If anything parses or re-serialises the request before you hash it, the string may no longer match what was signed.
Fix: hash the unmodified text, then parse. Confirm you are reading with request.text() and that no framework middleware is consuming the body first.
Notifications stop after a Designer change
Cause: someone renamed a form field. The keys in the payload are the field names, so a mapping built against "Work Email" produces nothing once that becomes "Email".
Fix: rename deliberately, then update the mapping and re-test. Treating field names as an interface rather than a label prevents the next occurrence.
The channel gets ignored within a fortnight
Cause: too many forms feed one channel, so the signal drowns. This is not a technical fault, but it is the most common way the whole exercise stops paying off.
Fix: split by intent or add a filter step, so the channel carries only leads someone is expected to act on. Signal returns almost immediately once the volume drops.
What you can build next with Slack and Webflow Cloud
Once a submission passes through code you control, Slack becomes one destination among several: the same handler can write the lead to a database, enrich it before posting, or hand it to a mail platform.
Our Mailchimp registration guide builds that second path in full using the same Route Handler shape. For the connection routes covered here without code, see the Webflow and Slack integration and the Webflow and Zapier integration.
For deeper customization beyond what these integrations handle, Webflow's developer docs cover Route Handlers, webhook verification, and the rest of the Webflow Cloud runtime.
Frequently asked questions
Do Webflow form webhooks fire from a preview?
Test on the published site rather than the Designer canvas. Webflow receives submissions on the webflow.io staging subdomain and on a custom domain with an active Site plan, so submit a real entry there.
Can I send leads to Slack without Zapier or code?
Slack's own incoming webhooks give you a URL that accepts a JSON body, documented in Slack's incoming webhooks guide. Something still has to receive the Webflow submission and reshape it into that body, so you need either an automation platform or a small handler in between.
Why verify the webhook signature if the URL is secret?
A URL is not a credential. It appears in logs, in browser history, in screenshots, and in whatever tool you pasted it into while debugging. Signature verification is what proves the request came from Webflow, and it is the difference between a notification channel and an open endpoint anyone can post into.
Which fields arrive in the payload?
Every field in the form, keyed by its name as set in the Designer, along with the form name, the form ID, the site ID, a schema array describing each field, a localeId, and a submission timestamp. Do not assume a type: the reference example shows a numeric field unquoted, so convert before formatting.
Should the handler wait for Slack before responding?
Answer the webhook promptly. Senders retry when they do not get a success response, and a slow downstream call can produce duplicate notifications for a single submission. If you add more work later, acknowledge first and do the rest in the background.




