Secure an n8n Webhook
Build a signed form-to-workflow path that rejects forged webhook requests.
Introduction
30 Second Summary
Forms often feel private because all you see is a submit button. Behind that button sits an address that strangers can target if they discover it.
In this project, you will protect an n8n webhook with a Next.js API route that signs form submissions using HMAC-SHA256. You'll test unsigned requests plus altered requests before deploying the working form to Webflow Cloud.
What You'll Build
You'll watch a live form submission reach the end of your n8n workflow while unsigned or altered copies stop at the signature check.
By the end of this project, you'll have:
- A signed form-to-workflow path where a submission through your API route appears as a completed n8n execution.
- A set of rejection tests where unsigned requests plus one-character payload changes stop at the signature check.
- A live Webflow Cloud form that sends a trusted request through your deployed route to the end of the workflow.
- An optional Secret Mission to push your webhook security skills further.
Are there any prerequisites?
You'll need Node, a Next.js app, a Webflow site, plus a publicly reachable n8n instance. Basic familiarity with terminal commands will help you follow the request tests.
Before We Start
Your webhook security chain starts in a local app. It finishes inside n8n.
This step prepares every tool before the workflow work begins. You will also choose the shared secret that later proves each request came from your route.
In this step, get ready to:
- Prepare the local development environment.
- Create your n8n workspace.
- Create your Webflow Cloud project.
Prepare the local tools
Webflow Cloud needs Node.js 22 or later for the local project. You will use curl to send test requests throughout the project.
- Check your installed Node.js version by running this command:
node --version
What should you see?
The command prints the installed Node.js version. Use the matching tab below to decide your next action.
✔️ I see version 22 or higher
Your Node.js runtime is ready. You can move on to the curl check.
ⓧ I see an older version
Your current runtime is below the version Webflow Cloud supports. Install an LTS release at version 22 or later.
- Visit the official Node.js download page.
- Select an LTS release at version 22 or later.
- Download the installer for your operating system.
- Run the installer using its default options.
- Return to your terminal after the installation finishes.
- Confirm the upgraded version by running this command:
node --version
Confirm the upgrade
The output should now show Node.js version 22 or later. This confirms the Webflow Cloud requirement is covered.
Still seeing the older version?
Close your terminal after the installer finishes. Start a new terminal session so it can find the updated Node.js installation.
If the old version remains, help me find why my terminal still uses an older Node.js installation.
ⓧ Command not found
Node.js is missing from your computer. The official installer adds the runtime that your local project needs.
- Visit the official Node.js download page.
- Select an LTS release at version 22 or later.
- Download the installer for your operating system.
- Run the installer using its default options.
- Start a new terminal session after the installation finishes.
- Confirm the installation by running this command:
node --version
Confirm the installation
The output should show Node.js version 22 or later. Your runtime is now available from the terminal.
Node.js still unavailable?
Restart your terminal first. If the command still fails, the installer may not have added Node.js to your system path.
Use this prompt to help me make Node.js available in my terminal.
curl gives you a direct way to send HTTP requests without building the form first. It lets you test each webhook layer as soon as you create it.
- Check that curl is available by running this command:
curl --version
What does this confirm?
The first output line shows the installed curl version. The remaining lines list the protocols supported by that installation.
Your terminal is ready when both Node.js and curl report version information.
curl not available?
Windows users can try the same check in Command Prompt if PowerShell handles curl differently. Other systems can use the installation options on the official curl download page.
If the command still fails, help me install curl for my operating system.
Create the hosted workspaces
n8n Cloud gives you a publicly reachable workspace without local hosting. Its free trial lasts 14 days.
Starting a trial can feel like a commitment. It expires without a charge if you do not upgrade. n8n then deletes the workspace.
- Open the official n8n Cloud trial guide.
- Use the trial link on that page to begin account setup.
- Complete the account form using credentials you can access again.
- Open your workspace from the n8n Cloud admin dashboard.
You should see the workflow overview inside your signed-in workspace. That confirms n8n is ready for the webhook you create next.
Your Webflow Cloud app stores its source in a GitHub repository. The deploy wizard creates both the repository and the first environment.
Why does Webflow request GitHub access?
The authorization screen can feel sensitive. Webflow Cloud needs repository access so it can create the starter project and deploy future code changes.
- Open the Webflow dashboard.
- Create a Webflow account if the dashboard prompts you to register.
- Select the Workspace that will hold your app.
- Click Create app in a populated Workspace.
- Choose Deploy app instead if the Workspace is empty.
- Click Login to GitHub in the deploy wizard.
- Authorize Webflow Cloud in the GitHub window.
- Install the Webflow Cloud GitHub App for your account when prompted.
Where are you now?
The deploy wizard can now create a repository in your GitHub account. Next you will use its Next.js starter as your local web project.
- Expand Clone a starter template in the deploy wizard.
- Select the Next.js starter template.
- Enter your-app-name as the app name.
- Choose your Webflow Workspace.
- Select the GitHub account that should own the repository.
- Choose New domain as the deployment target.
The first deployment can take a few minutes while Webflow creates the app environment. A short wait here is expected.
- Click Deploy to create the app.
That is the hosted foundation ready. You should see a successful deployment with an environment for your new app.
The generated repository now needs a local copy. Git keeps that copy connected to the source Webflow Cloud deploys.
- Open the generated GitHub repository from the Webflow Cloud success screen.
- Click Code above the repository file list.
- Select HTTPS in the repository menu.
- Copy the displayed repository URL here: https://github.com/YOUR-USERNAME/YOUR-REPOSITORY.
- Switch to your terminal.
- Clone the project onto your Desktop by running these commands:
cd ~/Desktop
git clone [[REPOSITORY_URL="https://github.com/YOUR-USERNAME/YOUR-REPOSITORY"]] [[APP_NAME="your-app-name"]]
cd [[APP_NAME="your-app-name"]]
What do these commands do?
- The first command moves your terminal to the Desktop.
- The second command downloads the repository into a folder named after your app.
- The final command enters that folder so the local project is open in your terminal.
- Check the final lines of your terminal output.
You should see the repository finish cloning. Your terminal prompt should now point to the your-app-name folder on your Desktop.
Repository not cloning?
Confirm that the HTTPS URL came from the repository created by Webflow Cloud. A private repository also requires permission through your signed-in GitHub account.
If your terminal cannot run Git, help me install Git and clone my Webflow Cloud repository.
Generate the shared secret
An HMAC check depends on both systems holding the same shared secret. Anyone with that value can create a valid signature.
The next command prints a live credential in your terminal. Keep it out of screenshots. Never paste it into a validation response.
- Generate 32 bytes of cryptographically strong random data by running this command:
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
What does this generate?
Node.js generates 32 random bytes through its cryptography module. Hex encoding turns those bytes into a 64-character value that works as the shared secret.
- Copy the generated value into your password manager under the name WEBHOOK_SIGNING_SECRET.
Protect this value
Use the same stored value later in the local server environment and the n8n configuration. Keep it outside client-visible code because browser visitors can inspect anything sent to the page.
No random value printed?
Confirm that the earlier Node.js version check succeeded. A missing runtime prevents the cryptography module from running.
If Node.js reports an error, help me generate the signing secret safely.
Your final check covers both hosted workspaces before you start building the webhook.
- Return to your signed-in n8n workspace.
You should see the workflow overview without another account setup screen.
- Return to your Webflow Cloud app's Environments Dashboard.
You should see the environment created by the starter deployment.
Before you run the final terminal check, do you expect both local tools to report version information?
- Switch back to the terminal inside your local project.
- Confirm both local tools by running these commands:
node --version
curl --version
Your readiness check
The first command should report Node.js version 22 or later. The second command should report curl version information.
You now have the local project, hosted workspaces, request tool, and protected signing secret ready. Every part of the webhook security chain has a place to live.
Your setup is complete. Next up, you will create the webhook and watch its first test request reach n8n.
Create the Raw-Body Webhook
A public webhook gives outside services a door into your n8n workflow. Before you secure that door, you need to prove it can receive the exact request data your form sends.
Signature checks depend on the original payload bytes. The existing WEBHOOK_SIGNING_SECRET stays untouched while you confirm that the webhook preserves those bytes.
In this step, get ready to:
- Create an n8n workflow with a Webhook trigger.
- Configure the webhook to accept POST requests at /secure-form with raw body enabled.
- Prove the endpoint captures a curl JSON request as raw body data.
Build the webhook trigger
A Webhook node starts the workflow when its URL receives a request. This node becomes the workflow's public entry point.
- Select Create Workflow from the n8n workspace overview.
- Select Add first step on the blank workflow canvas.
You should now see the nodes panel beside the empty workflow canvas.
- Search for Webhook in the nodes panel.
- Select Webhook to add it as the workflow trigger.
The Webhook node should now appear on the canvas with its configuration panel open.
- Select POST in the HTTP Method field.
- Replace the generated value in the Path field with /secure-form.
The node now exposes a POST endpoint whose displayed webhook URL ends with /secure-form.
Preserve the request bytes
An HMAC-SHA256 signature represents one exact sequence of bytes. Raw body mode keeps the incoming payload available in its original form for that later calculation.
- Select Add Option in the Webhook node.
- Select Raw Body from the available options.
You should now see the Raw Body setting in the node configuration.
- Turn on Raw Body.
The Webhook node is now configured to retain the incoming request body in raw form.
- Select Test URL at the top of the node panel.
- Copy the displayed URL into this field: your n8n test webhook URL.
The saved test URL should include the /secure-form path at the end.
Send a test POST
The test webhook registers for 120 seconds. A request sent during that window appears directly in the workflow editor for inspection.
- Select Listen for test event in the Webhook node.
Before you send the request, consider this question: will n8n preserve the JSON body exactly as it leaves curl.
- Switch to the terminal from earlier and send the test payload by running this command:
curl --request POST --header "Content-Type: application/json" --data-binary '{"name":"NextWork","message":"raw body test"}' [[N8N_TEST_URL="your n8n test webhook URL"]]
What does this request do?
- The --request option sends the request with the POST method expected by the Webhook node.
- The --header option labels the payload as application/json.
- The --data-binary option sends the supplied body bytes without changing them.
- The saved test URL directs the request to your /secure-form endpoint.
- Switch back to the n8n workflow editor.
- Select the completed Webhook node in the test execution.
- Inspect the node output for the received raw request body.
You should see a completed test execution containing the raw body for {"name":"NextWork","message":"raw body test"}.
Request missing from n8n?
- Select Listen for test event again if the 120-second test window expired.
- Confirm that the URL in your terminal matches the displayed Test URL.
- Confirm that Raw Body remains enabled in the Webhook node.
Ask for help with the exact result you see: Help me debug why my curl POST request is not appearing in my n8n Webhook test execution.
Your webhook now receives POST requests without losing the original body. Next, you will put your own server route in front of it and sign each payload before forwarding it.
Sign and Forward Requests
Your n8n webhook can already receive a raw JSON body. A caller still reaches it by knowing the URL.
The local API route becomes the only path your form needs. It attaches an HMAC-SHA256 proof to the untouched body.
In this step, get ready to:
- Keep the shared secret inside the local environment file.
- Build the POST route that signs raw request bytes before forwarding them.
- Send a curl request through the route to prove the signature reaches n8n.
Configure the server-only values
The API route needs the n8n webhook address plus the secret selected earlier. This is the sensitive part: anyone holding that secret can create a valid signature.
- Return to the n8n Webhook node from earlier.
- Copy its test URL into your n8n test webhook URL.
- In the local web project's Explorer sidebar, create .env.local beside package.json.
- Add the server configuration to .env.local by pasting the following lines:
# Keep the webhook target and signing key on the server.
N8N_WEBHOOK_URL=[[N8N_WEBHOOK_URL="your n8n test webhook URL"]]
WEBHOOK_SIGNING_SECRET=your-strong-shared-secret-here
What does this configuration do?
- N8N_WEBHOOK_URL tells the API route where to forward each request.
- WEBHOOK_SIGNING_SECRET supplies the private key used to calculate the signature.
- Both values stay in server-side configuration instead of entering browser code.
- Replace your-strong-shared-secret-here with the strong shared secret selected earlier.
- Save .env.local.
- Confirm .env.local appears beside package.json in the Explorer sidebar.
Environment file missing?
Check that the filename begins with a dot. Confirm the file sits beside package.json instead of inside the app folder.
Help me check my local environment file.
Build the signing route
A Next.js route handler can read the request as raw bytes. The Node.js crypto module signs those bytes before the route forwards the same buffer.
- In the local web project's Explorer sidebar, create app/api/submit/route.js.
- Add the signing handler to app/api/submit/route.js by pasting this code:
import { createHmac } from "node:crypto";
export async function POST(request) {
// Preserve the exact bytes used to calculate and verify the signature.
const body = Buffer.from(await request.arrayBuffer());
const secret = process.env.WEBHOOK_SIGNING_SECRET;
const webhookUrl = process.env.N8N_WEBHOOK_URL;
if (!secret || !webhookUrl) {
return new Response("Missing server configuration.", { status: 500 });
}
// Produce the hexadecimal proof attached to the forwarded request.
const signature = createHmac("sha256", secret).update(body).digest("hex");
// Forward the same body bytes with the signature in a request header.
const webhookResponse = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": request.headers.get("content-type") ?? "application/octet-stream",
"X-Webhook-Signature": signature,
},
body,
});
return new Response(await webhookResponse.text(), {
status: webhookResponse.status,
});
}
What does this code do?
- request.arrayBuffer() reads the original request body without parsing its JSON structure.
- createHmac() combines the raw body with WEBHOOK_SIGNING_SECRET to produce a hexadecimal digest.
- fetch() forwards the same body buffer to the n8n webhook URL.
- X-Webhook-Signature carries the digest that n8n can verify later.
- Save app/api/submit/route.js.
- In the terminal panel from earlier, press Ctrl+C if the local development server is already running.
- Start the local development server with the new environment values by running:
npm run dev
What does this command do?
The development script starts the local Next.js server. Restarting it loads the values saved in .env.local.
You should see the development server start with http://localhost:3000 available locally.
Route not starting?
Confirm the terminal is running from the folder containing package.json. Check that route.js sits inside app/api/submit.
Help me debug my signing route.
✔️ Awesome, I've got everything!
Great. Double-check that app/api/submit/route.js is saved before testing the request.
ⓧ I'd like to double check the full code
Compare your route with this complete reference.
import { createHmac } from "node:crypto";
export async function POST(request) {
// Preserve the exact bytes used to calculate and verify the signature.
const body = Buffer.from(await request.arrayBuffer());
const secret = process.env.WEBHOOK_SIGNING_SECRET;
const webhookUrl = process.env.N8N_WEBHOOK_URL;
if (!secret || !webhookUrl) {
return new Response("Missing server configuration.", { status: 500 });
}
// Produce the hexadecimal proof attached to the forwarded request.
const signature = createHmac("sha256", secret).update(body).digest("hex");
// Forward the same body bytes with the signature in a request header.
const webhookResponse = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": request.headers.get("content-type") ?? "application/octet-stream",
"X-Webhook-Signature": signature,
},
body,
});
return new Response(await webhookResponse.text(), {
status: webhookResponse.status,
});
}
This reference preserves one body buffer for both signing and forwarding.
Prove the signature reached n8n
The route now has one job to prove. n8n must receive the original JSON body with a signature header attached.
- Return to the n8n workflow from earlier.
- Put the Webhook node into test listening mode for the next request.
Before you send it, which two pieces of evidence would prove that the route signed the exact bytes it forwarded?
- Send a JSON body through /api/submit by running this command in a second terminal panel:
curl -i -X POST http://localhost:3000/api/submit \
-H "Content-Type: application/json" \
--data-raw '{"name":"Ada","message":"Signed request"}'
What does this request prove?
curl sends the JSON to your local API route. The route signs those bytes before forwarding them to the n8n test webhook.
- Return to the newest n8n execution created by the request.
- Select the Webhook node's received input data.
- Locate X-Webhook-Signature in the received headers.
- Compare the raw body with {"name":"Ada","message":"Signed request"}.
You should see a hexadecimal value in the signature header. The raw body should still contain the exact JSON sent by curl.
That is the handoff working: n8n received the untouched body plus its HMAC proof.
No signed execution?
Confirm the Webhook node was listening before the request ran. Restart the development server after every change to .env.local.
Help me trace my signed request.
Your API route now signs every body before n8n receives it. Next, you'll make the workflow reject any request that lacks valid proof.
Verify the Webhook Signature
Your API route now signs each payload before forwarding it to n8n.
The last webhook execution exposed the signature header. It also showed the untouched body.
A public endpoint still accepts forged calls until the workflow verifies each signature. That missing gate is the problem you'll close now.
n8n will recompute HMAC-SHA256 from the raw bytes. A matching request continues to the successful end.
In this step, get ready to:
- Store the shared secret in an n8n Crypto credential.
- Verify each request against a freshly computed HMAC-SHA256 digest.
- Prove that an unsigned curl request stops at the check.
Recompute the signature from raw bytes
HMAC requires identical bytes. It also requires the same secret on both sides.
The Webhook node already preserves the raw body in binary property data. The Crypto node can calculate over that property directly.
- Switch back to the n8n workflow from earlier.
- Select the Add node connector on the right side of the Webhook node.
- Enter Crypto in the nodes panel search field.
- Select Crypto from the results.
The Crypto configuration panel should now be visible. This node becomes the first checkpoint after the Webhook node.
- Set Action to Hmac.
- Select Credential to connect with.
- Choose the option to create a Crypto credential.
This is the sensitive handoff. n8n masks the value after you save the credential.
- Switch back to the local environment file from earlier.
- Copy the value assigned to WEBHOOK_SIGNING_SECRET.
- Return to the new Crypto credential in n8n.
- Paste the copied value into Hmac Secret.
- Save the credential.
Good, the shared secret is now available to the workflow. Its saved value stays masked.
- Turn on Binary File.
The Binary Property Name field should now appear. It tells Crypto where the untouched request bytes live.
- Enter data in Binary Property Name.
- Set Type to SHA256.
- Enter signature in Property Name.
- Set Encoding to HEX.
- Select Publish.
Before you resend the signed request, do you expect the new digest to match the header created by your API route?
- Switch back to the terminal from earlier.
- Press the Up Arrow until the earlier signed request to /api/submit appears.
- Press Enter.
- Return to Executions in n8n.
- Open the newest execution.
- Select the Crypto node in that execution.
You should see a hexadecimal digest in signature. That proves n8n hashed the received raw body with the shared secret.
No Digest in the Crypto Output?
- Confirm that Raw Body remains enabled on the Webhook node.
- Confirm that Binary Property Name contains data.
- Confirm that the saved Hmac Secret matches the local value exactly.
Help me debug the missing HMAC digest in my n8n Crypto node.
Gate the workflow on a match
The freshly computed digest now needs a decision point. An If node can compare it with the incoming signature header.
- Select the Add node connector on the right side of the Crypto node.
- Enter If in the nodes panel search field.
- Select If from the results.
- Choose String as the condition data type.
- Drag signature from the input data into the first value.
- Select is equal to as the comparison.
- Expand the incoming headers in the input data.
- Drag the field containing X-Webhook-Signature into the second value.
How Does the Gate Work?
- Crypto writes the recomputed digest to signature.
- If compares that digest with the request header.
- The true output receives matching requests.
- The false output receives unsigned requests or mismatched signatures.
- Add a No Operation, do nothing node to the true output.
- Leave the false output unconnected.
- Select Publish.
Before you test the gate, which branch should receive a request signed by /api/submit?
- Switch back to the terminal from earlier.
- Press the Up Arrow until the signed request to /api/submit appears.
- Press Enter.
- Return to the n8n execution list.
- Open the newest execution.
The true connector from If should be highlighted. The No Operation node should be green.
Your signed request now reaches the workflow's successful end. The gate accepts the payload because both digests match.
Signed Request Taking the False Branch?
- Confirm that the first comparison value comes from signature.
- Confirm that the second comparison value comes from X-Webhook-Signature.
- Confirm that the true output connects to the No Operation node.
Help me debug why my signed webhook request is taking the false branch.
Prove unsigned requests stop
An unsigned request still carries a body. It carries no proof that your API route sent it.
The false output should receive this request. Its unconnected path ends at the signature check.
- Select the Webhook node in the workflow from earlier.
- Copy its active webhook URL.
- Record the copied value here: your n8n webhook URL.
Before you run this request, do you expect it to reach the No Operation node?
- Send JSON directly to n8n without a signature by running this command:
curl --header "Content-Type: application/json" --data-binary '{"name":"Unsigned test"}' [[N8N_WEBHOOK_URL="your n8n webhook URL"]]
- Return to the n8n execution list.
- Open the newest execution.
The Webhook node and Crypto node should be green. The If node should route the request through its false output.
The No Operation node should remain untouched. That stopped path proves the unsigned request cannot reach the workflow's successful end.
Unsigned Execution Missing?
- Confirm that your saved webhook URL is still active.
- Confirm that the workflow was published after adding the If node.
- Confirm that the command targets the n8n webhook URL directly.
Help me find why my unsigned request is missing from n8n executions.
Your webhook now rejects callers that never knew the shared secret. Next, you'll alter a signed body and prove that the same gate catches tampering.
Reject a Tampered Payload
Your n8n workflow already drops requests with no signature. That proves your webhook requires proof of origin.
A copied signature could still look convincing beside an altered body. In this step, you will change one character after HMAC signing to test whether the verification check detects the mismatch.
In this step, get ready to:
- Capture a successful signed request as your control.
- Resend its original signature with a one-character body change.
- Confirm that the tampered execution stops at the signature check.
Capture a valid control request
A control request proves that the route still signs the original body successfully. Its signature becomes the fixed value for your tampering test.
- Switch back to the terminal from earlier.
- Record the full local URL you used earlier for /api/submit: your local /api/submit URL
- Send the original control body through your local route by running this command:
curl --data-binary '{"name":"Ada","message":"Hello"}' --header "Content-Type: application/json" [[LOCAL_SUBMIT_URL="your local /api/submit URL"]]
What Does This Command Prove?
- The --data-binary option sends the exact JSON bytes shown in the command.
- Your local route signs those bytes with WEBHOOK_SIGNING_SECRET.
- The route forwards the unchanged body with its hexadecimal digest in X-Webhook-Signature.
- Return to the n8n workspace from earlier.
- Open the workflow's newest execution.
- Select the Webhook node in the execution view.
You should see Hello in the received body. The execution should continue through the signature check to the workflow's successful end.
Good. Your successful control now gives the tampering test a trustworthy baseline.
The header value sits inside the Webhook node's execution data, so it can take a moment to spot.
- Copy the value beside X-Webhook-Signature from this successful execution.
This copied signature authenticates only the exact control body. Using it for this test does not reveal your shared secret.
No Successful Control Execution?
- Restart the local development server with the project command you used earlier if the route is unavailable.
- Check that the local route URL ends with /api/submit.
- Confirm that the local environment still contains WEBHOOK_SIGNING_SECRET.
Help me diagnose why my signed control request is not reaching n8n.
Send the one-character change
An HMAC digest depends on every byte in the body. This test keeps the signature from the control request.
The new body changes only Hello to Jello. The original signature stays untouched.
- Return to the existing n8n workflow.
- Select the Webhook node.
- Record the full webhook URL shown in the node: your n8n webhook URL
- Replace paste-original-signature-here in the command below with the signature copied from the successful control execution.
Before you run this test, do you expect the old signature to match after one character changes?
- Send the tampered body directly to the n8n webhook by running this command:
curl --data-binary '{"name":"Ada","message":"Jello"}' --header "Content-Type: application/json" --header "X-Webhook-Signature: paste-original-signature-here" [[N8N_WEBHOOK_URL="your n8n webhook URL"]]
What Changed in This Request?
- The body contains Jello instead of the signed value Hello.
- The X-Webhook-Signature header still contains the digest calculated for the original body.
- The direct webhook URL prevents your local route from creating a fresh signature for the changed body.
- Return to the n8n workspace from earlier.
- Open the workflow's newest execution.
- Select the Webhook node in the execution view.
You should see Jello in the received raw body. This proves the altered request reached the workflow.
- Select the signature-verification check immediately after the Webhook node.
You should see the execution stop at the signature check. The workflow's successful end is never reached.
That is the integrity check proven. A single changed character invalidates the original signature.
Did the Tampered Request Continue?
- Confirm that the body in the second command contains Jello.
- Confirm that the header contains the signature copied from the Hello control execution.
- Confirm that the second command targets the n8n webhook URL instead of the local /api/submit route.
Help me find why my one-character-tampered request passed the n8n signature check.
- Keep the request headers outside your screenshot so the copied signature is not visible.
Your workflow now rejects requests with no signature. It also rejects signed requests whose body changes.
Next, you will connect the form to your protected submission path and follow a valid request to the workflow's successful end.
Submit Through the Form
Your unsigned request stopped at the n8n webhook's signature check. Your one-character tampered request stopped there too.
Now the browser form needs to reach the same workflow through /api/submit. The server route signs each payload with HMAC-SHA256 while WEBHOOK_SIGNING_SECRET stays outside the browser.
In this step, get ready to:
- Connect a local form component to the signing route.
- Submit a test payload from the local page.
- Trace the signed request to the workflow's successful end.
Connect the form to the signing route
The form needs one server-side destination. That boundary lets the browser submit data without gaining access to the shared secret.
Why the route stays in the middle
The form sends its payload to /api/submit. That server route creates the signature before forwarding the unchanged body.
Keeping the direct n8n webhook URL out of browser code hides the backend destination. Keeping the secret on the server prevents visitors from signing their own requests.
- Switch back to the local web project that is already open.
- Add a form component whose submit behavior sends its payload to /api/submit.
- Save the form component.
- Start the project's local preview with its existing development command.
You should see the form rendered on the local page. It is now ready to send a browser request through your signing route.
- Search the form component source for /api/submit.
You should find /api/submit in the form's submission behavior.
- Search the form component source for /secure-form.
- Search the form component source for WEBHOOK_SIGNING_SECRET.
Both searches should return no occurrence in the form component. This confirms the browser code contains neither the direct webhook path nor the shared secret.
Form missing from the local page?
- Confirm the current page includes the new form component.
- Check that the form submission target matches /api/submit exactly.
- Review the terminal for a compilation problem in the form component.
Still stuck? Help me find why my local form is missing or cannot submit to the API route.
Submit the form locally
This browser submission follows the valid signing path you tested earlier. The browser supplies the payload while the server route handles the signature.
- Return to the n8n workflow from earlier.
- Leave the workflow ready to receive the next webhook request.
- Switch back to the form's local preview.
- Enter test values in every required form field.
- Predict which path the execution will follow when the browser sends the request.
- Submit the form using its submit control.
Your submission sends one request through /api/submit. The execution history provides the proof that the server signed it correctly.
Trace the successful execution
The earlier rejection runs give you a useful comparison. The newest execution should follow a longer path because its body and signature still match.
- Return to the n8n execution list.
- Open the newest execution created by the local form submission.
- Inspect the signature-verification check.
- Follow the execution path to the workflow's successful end.
- Compare the newest execution with the earlier unsigned execution.
- Compare the newest execution with the earlier tampered execution.
You should see the local form request pass the signature check. You should see that execution reach the workflow's successful end while both earlier rejection runs remain stopped at the check.
That closes the local loop. Your real form now passes the check that blocked both forged requests.
No successful execution?
- Confirm the form sends its request to /api/submit.
- Confirm the local environment still uses the same WEBHOOK_SIGNING_SECRET as n8n.
- Submit a fresh form request after saving any correction.
Need another set of eyes? Help me debug why my local form submission does not pass the n8n signature check.
Your local form now reaches the verified n8n workflow through the server route. Next, you'll prove the same path from a live Webflow Cloud deployment.
Deploy and Test the Live Form
Your local form already sends a signed request through /api/submit. That request reaches the successful end of your n8n webhook workflow.
Webflow Cloud runs the deployed API route in a separate environment. The live route needs the same signing secret before it can protect a public submission.
You will deploy the app to its live URL. Then you will trace one live submission through the complete protected workflow.
In this step, get ready to:
- Configure the shared signing secret in the Webflow Cloud deployment environment.
- Deploy the web project to Webflow Cloud.
- Capture proof from a successful live workflow execution.
Configure the deployment secret
An environment variable gives your server-side route access to configuration outside the source code. This secret lets /api/submit produce the HMAC-SHA256 signature that n8n verifies.
Your signing secret is the only value that can authenticate this route. Webflow Cloud can store it in masked form.
- Return to the Webflow Cloud project from earlier to configure its live environment.
- Select your app to open the Environments Dashboard.
- Select the environment that serves your live site.
- Click Environment Variables.
- Click Add variable.
- Select Add single variable.
Why Use a Secret Variable?
Webflow Cloud makes environment variables available during the build. The deployed app can also read them at runtime.
The Secret setting encrypts the value. It also masks the value in the Environment Variables dashboard.
- Enter WEBHOOK_SIGNING_SECRET in the Key field.
- Return to the local environment file from earlier.
- Copy the existing value assigned to WEBHOOK_SIGNING_SECRET.
- Paste the copied value into the Value field.
- Enable Secret.
- Click Add variable.
The sensitive part is handled. You should see WEBHOOK_SIGNING_SECRET listed with a masked value.
Signing Secret Missing or Visible?
- Check that the key is exactly WEBHOOK_SIGNING_SECRET.
- Confirm that Secret is enabled if the value remains readable in the dashboard.
Help me check why my Webflow Cloud signing secret is missing or unmasked.
Deploy the Webflow Cloud app
A new deployment makes the environment variable available to the live /api/submit route. It also publishes the form at the app's live URL.
- Return to the selected environment's Environments Dashboard.
- Start the deployment by clicking Deploy latest build.
The build can take several minutes while Webflow Cloud prepares the app. A quiet progress screen during this time is expected.
- Wait for the deployment to finish.
You should see a successful deployment in Deployment history. The app's live URL should also be available.
- Open the live URL to load the deployed form.
You should see the same form that worked locally. This confirms that Webflow Cloud serves the deployed form.
Deployment Did Not Succeed?
- Select the failed deployment's Deployment ID (Commit SHA).
- Review the Build logs to identify the failing build step.
- Confirm that WEBHOOK_SIGNING_SECRET is listed if the site loads but the live form cannot submit.
Help me diagnose my Webflow Cloud deployment failure.
Submit and trace the live form
A live submission tests the exact public path your visitors use. Its n8n execution should pass the signature check before reaching the successful end.
- Enter Live Webflow Cloud test in one free-text field.
- Complete every remaining required field with valid test data.
Before you submit, do you expect the live request to stop at the signature check or reach the successful end?
- Submit the live form using its submit button.
Your browser should complete the submission without showing a request error.
- Return to the n8n workflow from earlier.
- Select the Executions tab in the top menu.
- Select the newest execution that matches the live submission time.
- Confirm that the execution continues beyond the signature check to the workflow's final node.
You should see a Success status for the live submission. The earlier unsigned request and tampered request should remain visible as stopped executions.
- Frame the execution view around the completed workflow path.
- Keep the Success status visible in the frame.
- Capture a screenshot with the live execution time visible.
You now have production proof that the public form can trigger your protected workflow.
Secret mission
Build a Webhook Security Evidence Pack
Turn your successful, unsigned, and tampered n8n executions into a security evidence pack. Show exactly how the signature check handles each request.
Clean Up Your Resources
Clean Up Your Resources
That completes the security loop: your live form reaches n8n while forged requests stop at the check. Ongoing costs depend on your current Webflow Cloud plan plus your n8n hosting choice.
Resources you used:
- Your deployed Webflow Cloud project serving the live form through /api/submit.
- Your n8n workflow containing the POST Webhook node at /secure-form.
- Your n8n execution records from every rejection test plus each successful submission.
- Your local web project containing the local environment file with WEBHOOK_SIGNING_SECRET.
- Your successful live execution screenshot.
Keep everything running
This option preserves your working live path. Secret rotation touches two live systems. Both copies need the same new value before your next submission.
- Create a new strong shared secret in your usual secure credential storage.
- Replace the Webflow Cloud deployment value for WEBHOOK_SIGNING_SECRET with the new secret.
- Replace the n8n verification value for WEBHOOK_SIGNING_SECRET with the same new secret.
- Redeploy the Webflow Cloud project.
- Remove the old WEBHOOK_SIGNING_SECRET entry from the local environment file.
- Submit the live form once.
You should see a new n8n execution reach the successful end. That confirms the rotated secret is working.
Pause - I'll come back to this later
Pausing closes the webhook request path while preserving your project artifacts. It also removes the shared secret from active configuration.
- Deactivate the workflow in n8n.
- Remove the Webflow Cloud deployment value for WEBHOOK_SIGNING_SECRET.
- Remove the n8n verification value for WEBHOOK_SIGNING_SECRET.
- Redeploy the Webflow Cloud project.
- Remove the WEBHOOK_SIGNING_SECRET entry from the local environment file.
- Submit the live form once after redeployment.
You should see no successful workflow run from the paused submission. Your project files plus execution evidence remain available for your return.
Delete - I don't want to use this again
Deletion is permanent. This option removes the live path plus its local artifacts.
- Delete the execution records linked to /secure-form using the record deletion controls in the n8n execution list.
- Delete the n8n workflow using the deletion control in its workflow settings.
- Delete the deployed Webflow Cloud project using the deletion control in that project's settings.
- Delete the local environment file containing WEBHOOK_SIGNING_SECRET using your computer's file browser.
- Delete the folder containing the local web project using your computer's file browser.
- Delete the successful execution screenshot from its saved location.
- Empty your computer's deleted-items folder.
- Visit the former live site URL.
You should see that the deployed form no longer loads. The public route can no longer forward requests to the deleted workflow.
Nice Work!
Nice Work!
You did it. Your live Webflow Cloud form now reaches n8n through a signed API route that blocks forged payloads.
What you learned:
- Built a server-side signing route at /api/submit. The route protects each unchanged payload with HMAC-SHA256. The browser never receives the shared secret. The browser never receives the n8n webhook URL.
- Configured a raw-body signature check in n8n. Unsigned requests stop before workflow actions run. Altered payloads stop there too.
- Deployed the form to Webflow Cloud. Traced a live signed submission through the n8n execution list. Captured its successful end as proof.
- Secret Mission: Completed an optional webhook security challenge to push your skills further.
Ready to quiz yourself?