Security considerations
Your webhook URL is public once embedded — here's how to protect the workflow behind it.
Messages travel straight from your visitor's browser to your n8n workflow, which is what keeps your data out of Stylchat's hands. The trade-off is that the embed snippet contains your webhook URL in plain text, and anyone who views your page source can read it. That's true of any widget that calls your workflow directly, and it can't be hidden — a URL the browser has to call is a URL the visitor can see.
In practice this means someone could send requests to your workflow without going through your widget. Whether that matters depends entirely on what your workflow does: a workflow that answers questions from a public FAQ has little to lose, while one that calls a paid AI model or writes to your database has real exposure. Treat your webhook URL as public information and put protection in the workflow itself.
Start with these
Worth doing for every workflow, regardless of what it does:
- Rate limit in front of n8n. Put a proxy such as Cloudflare in front of your n8n instance and set a per-IP rate limit on the webhook path. This is usually the single highest-value change you can make — it caps how much damage an automated script can do in an hour, and it takes an afternoon to set up on a free plan.
- Validate the message before doing any real work. Add an IF node right after your Webhook node that rejects anything malformed — an empty or missing
chatInput, or a message longer than you'd ever expect (say 2,000 characters). Put this before any node that costs money, so junk requests are discarded cheaply. - Watch your invocation volume. Abuse of a public URL gets caught by noticing it, not by preventing it. A scheduled n8n workflow or your proxy's analytics can alert you when hourly requests jump well above your normal traffic.
If your workflow costs money to run
Any workflow that calls a paid AI model or metered API needs a spending floor beneath it:
- Set a hard budget cap with your AI provider. OpenAI, Anthropic, and most metered APIs let you set a monthly limit. This doesn't stop abuse — it stops abuse from turning into an unexpected invoice, which is usually the real concern.
- Use a dedicated API key for this workflow. If the key is shared with your other systems, a runaway loop here takes those down with it. A separate key contains the problem to one workflow.
Domain lock
On the Connect tab, right under your webhook URL, there's a Domain lock field. Set it to the domain you're embedding on and Stylchat refuses to start the widget on any other site, so a copied snippet stops working.
A widget locks to one domain. Type over it and press Save to move the lock; create a separate widget if you need the same chat on a second site. Unlike the rest of the editor this field waits for Save rather than saving as you type, so a half-typed domain never goes live and starts turning your own visitors away.
The field is marked required, and a widget with no domain set warns you — but it does still run anywhere, which is what every widget created before this existed does. Nothing stops working until you set a domain.
You can also check this inside the workflow itself. Add an IF node after your Webhook node comparing {{ $json.headers.origin }} against your own domain and stop the workflow if it doesn't match.
Both approaches reliably stop someone copying your snippet onto a different website, because browsers set the Origin header themselves and page scripts can't fake it. Neither stops requests sent outside a browser, where that header can be set to anything. Useful, but don't treat either as a lock — they're filters.
Geofencing
Also on the Connect tab, below Domain lock, is Geofencing. Add one or more two-letter ISO country codes (for example US, GB, DE) to restrict the widget to visitors in those countries. With no codes added, the widget runs for visitors anywhere.
Verifying requests came from your widget
Every message your widget sends includes a stylchatToken field in the request body — a short-lived signed value that Stylchat issues to the page the widget is running on. Requests replayed from outside a browser won't have a valid one, so checking it filters out the most casual abuse.
The token is a SHA-256 HMAC of widgetId.origin.expiresAt, signed with a secret unique to your widget. Find it on the Connect tab under Signing secret — click to reveal, then store it in n8n as a credential rather than pasting it into the workflow itself.
To verify a message, add a Code node after your Webhook node that recomputes the HMAC and compares it to the token that arrived:
const crypto = require('crypto');
const secret = 'your-signing-secret';
const { stylchatToken, stylchatTokenExpiresAt, widgetId } = $json.body;
const origin = $json.headers.origin || '';
if (!stylchatToken) {
throw new Error('Missing Stylchat token');
}
// expiresAt is part of what was signed, so it arrives alongside the token.
// Check it first — otherwise an old token would still verify forever.
if (Date.now() > Number(stylchatTokenExpiresAt)) {
throw new Error('Expired Stylchat token');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${widgetId}.${origin}.${stylchatTokenExpiresAt}`)
.digest('hex');
const a = Buffer.from(stylchatToken, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error('Invalid Stylchat token');
}
return $input.all();
The expiry is checked before the signature because the signature alone proves only that Stylchat issued the token at some point — not that it's still current. A token is also tied to the exact domain it was issued for, so one lifted from your site won't verify anywhere else.
If your signing secret leaks
If the secret ends up somewhere it shouldn't — a screenshot, a shared workflow export, a support thread — open the Connect tab and click Rotate secret. A new one is generated on the spot and the old one stops working immediately.
Update your n8n credential to the new value straight after. Between rotating and updating, any workflow that verifies tokens will reject messages, so do the two together rather than leaving a gap. You don't need to re-copy or re-paste the embed snippet — the secret isn't part of it.
If a URL gets abused
n8n webhook URLs contain a long random ID. If you find yours being hit by traffic you didn't expect, regenerate the webhook URL in n8n, then re-copy the snippet from the Embed tab and replace it on your site. Every old copy stops working immediately.
