Why Your AI Support Widget Should Not Have a Public URL
September 15, 2026 · 9 min read · by the hiy team
The short answer
An AI support widget that opens for anyone who finds its address has four problems: your message allowance can be drained by a script, your documentation can be extracted through the answers, the widget can be embedded on a site you do not control, and any facts it is told about the visitor can be forged. The fix is to give the agent no public link at all and let visitors in with a signed, short-lived token that your own server mints per page view — your site vouches for its visitors, so they never need accounts with the agent's vendor and you never send the vendor your user list. The token names one agent, expires within minutes, is signed with a secret that never leaves your server, and when it is missing, expired or forged the agent should answer exactly as it would for an address that does not exist: not found.
This is how hiy's Support Agents work, and the design is small enough to explain in full.
What goes wrong with a public support widget?
A public chat widget is a public API with a friendlier face. Four things follow.
The allowance gets drained. Every AI answer costs the vendor money and costs you allowance — messages, credits, outcomes, whatever the plan counts. A public endpoint can be hit by a loop. On a per-outcome or per-credit plan that is a bill; on a flat plan with a cap it is your real customers locked out until the month turns over.
The corpus gets extracted. An agent that answers from your documentation will, given enough questions, reproduce most of it. For a public help centre that may not matter. For an agent that also knows your Known issues list, your internal limits, or anything you have not published, it does.
The widget gets embedded elsewhere. A public snippet works on any page. A competitor, a phishing page, or a fan site can mount your support agent under their own header, and your agent will answer in your product's name on a page you have never seen.
Visitor facts get forged. The useful version of a support agent knows who it is talking to — the plan they are on, the workspace they own — so it can answer "what's my limit?" without asking. If that information arrives as a query parameter, anyone can claim to be anyone.
A public key in the page solves none of these, because a key that is in the page is a key that everyone has.
How does token gating work?
The server that renders your page is the only party that knows the visitor is real, so it is the party that vouches. Five stations.
Your server renders the page for a visitor it already knows.
your session, your rules
A small payload, signed with a secret that never leaves the server.
HMAC-SHA256
The snippet takes the token; the snippet without one is inert.
data-token
Signature, the agent it names, and an expiry within the hour — all three, every request.
audience-bound
A valid token gets the agent. Anything else gets exactly what a made-up address gets.
fail closed
What the token carries
In hiy the token is v1. + a base64url payload + . + a base64url signature, and the payload is four fields: the agent's id (twin), your own user id for the visitor (sub), when it was issued (iat) and when it expires (exp). The signature is HMAC-SHA256 over the versioned payload with your signing secret. The exact minting code, in Node.js and as recipes for Next.js, WordPress and a static host, is in the developer docs and on the agent's Publish tab — the same constant in both places, with a test in the repo that runs it through the real verifier.
Four rules are enforced on every request, and each one closes one of the four problems above:
- Audience-bound. The
twinfield must name the agent being addressed. A valid signature is not enough: a token minted for one agent never opens another, even inside the same organisation. - One hour, maximum. A token whose
expis more than an hour after itsiatis refused even though it signed correctly. The lifetime is the verifier's policy, not the signer's; fifteen minutes is the sensible default, and sixty seconds of clock skew is tolerated either way. - Server-side only. The secret signs on your server. Shipping it to a browser hands every visitor the ability to mint a token for any of your users.
- Per organisation. Your secret is derived from a server master secret, so it is never sitting in a table waiting to leak, and holding it tells you nothing about anyone else's.
Why it is deliberately not a JWT
A JWT would work here, and we chose not to use one. A JWT carries an alg header that tells the verifier which algorithm to use, which is the origin of a whole family of attacks — alg: none, algorithm confusion between RSA and HMAC, key-discovery headers pointing at attacker-controlled keys. Those are solved problems in well-maintained libraries, and they are also problems a bearer credential for one agent for a few minutes does not need to have.
So there is exactly one algorithm, it is not negotiable, and there is no field in which to negotiate it. The token is not a general-purpose identity; it is a note from your server saying this visitor may talk to this agent until this time, and nothing else.
| Property | A public key in the page | A signed token from your server |
|---|---|---|
| Who can open the agent | Anyone who reads the page source | Visitors your server chose to vouch for |
| How long it works | Until you rotate the key, which breaks every page | Minutes; a new one per page view |
| Which agents it opens | Whatever the key was scoped to, often all of them | Exactly one, named in the payload |
| What it can say about the visitor | Nothing trustworthy; anything in the URL can be edited | Facts your server attested, readable for one answer and never stored |
| What a bad one returns | Often a helpful error naming the problem | The same not-found a made-up address gets |
Why a missing token looks like a 404
A restricted agent has no public page. Ask for its address without a token and you get the same "not found" a made-up slug returns; ask with an expired token, a bad signature, or a token for a different agent, and you get the same again. Those responses are deliberately indistinguishable. An error that says "token expired" confirms the agent exists and tells an attacker which of their guesses was close. A flat 404 says nothing.
The cost of that choice is on the honest side of the ledger: your own integration cannot tell an expired token from a wrong slug by the response alone, so a client holding a token should check the expiry itself before sending. That is a small price for an agent that does not announce itself.
What must not go in the token
The token is readable by the visitor, and in the embed it rides in a URL — the loader puts it in the frame's address, so it reaches browser history, the Referer on outbound links, and any log that records URLs. Two consequences:
- Nothing the visitor is not entitled to see about themselves. A token can carry a few attested facts so an action does not have to ask for what your site already knows — plan, workspace, account id. It must never carry someone else's data, an internal flag, or anything you would not print on the visitor's own settings page.
- Nothing that should outlive the answer. In hiy, attested values are readable by an action for the duration of one answer and are never written to the session row, the leads table, or anything joined to them. There is nothing to delete afterwards because nothing was kept.
The trade-off: you need a server
Token gating means something has to mint tokens, which means a static site alone cannot host a restricted agent — the recipes cover a static host by putting the minting on a small function in front of it. That is a real cost for a marketing site, and it is why hiy's personal twins and their public pages work the other way: anyone with the link, no token, because a public expert's page is meant to be public.
Two things soften it. The Publish tab's See it answer mints a ten-minute token and mounts the real widget right there, so you can watch the agent work before you have written a line of server code — a token you can never copy, so it cannot be mistaken for one to ship. And the same snippet works with a token whether you render it from Next.js, Node, WordPress or a function in front of static files; the recipes are four versions of one thing.
The honest version
A token proves your server vouched for the visitor; it does not prove the visitor is who your server thinks. If your login is weak, the token inherits it. Token gating protects the agent from the open internet, not from your own authentication bugs.
There is no self-service rotation of the signing secret yet. If it leaks — a screenshot of the Publish tab, a commit that should not have happened — you tell us and we rotate it for you, which is a slower loop than it should be and is on the list. And because the token rides in a URL for the embed, "nothing the visitor may not see" is a rule you have to keep, not one the system can keep for you.
Where to start
If your current widget has a public key in the page, ask the vendor what a script that loops on it would cost you, what a competitor embedding it on their site would see, and what happens to a visitor fact passed in the URL. If any answer is uncomfortable, the fix is architectural rather than a setting. A hiy Support Agent has no public link by construction — the three-step wiring on the Publish tab is copy the secret, mint a token per page view, drop the snippet — and the developer docs have the whole verifier's rules on one page.
Questions people ask
How do you secure an embedded AI chat widget?
Give it no public address and let visitors in with a signed, short-lived token your own server mints per page view. The token names one agent, expires within minutes, and is signed with a secret that never leaves the server; a missing, expired or forged token gets the same not-found response a non-existent address would.
Why not use a JWT for a chat widget token?
A JWT carries an algorithm header the verifier has to trust, which is where alg-none and algorithm-confusion attacks come from. A bearer credential for one agent for a few minutes does not need negotiable algorithms, so hiy’s token has exactly one — HMAC-SHA256 — and no field in which to choose another.
Can a support agent know which customer it is talking to without a login?
Yes, if your server puts attested facts — plan, workspace, account id — into the signed token. The agent can read them for the duration of one answer. Because the token is readable by the visitor and travels in a URL, it must carry nothing the visitor is not entitled to see about themselves, and hiy never stores those values after the answer.