Origin guard & rate limiting
Your widget endpoints are public by necessity — a browser on any visitor's device has to reach them. Two protections keep that from being a liability: an origin guard restricting which websites may use your widget, and rate limiting absorbing spikes and abuse.
Rate limiting needs no configuration. The origin guard needs one thing from you: the list of domains you trust.
Origin guard (allowed domains)
Edit the list under Dashboard → Widget → Install → Allowed domains (up to 20 entries).
With an empty list, every origin is accepted. This keeps first-time setup painless — paste the snippet, it works. The moment you add one domain, the guard activates and everything not on the list is refused with a 403. So the list is not a filter you tune; it is a switch you flip by using it. Add your domains as soon as the widget is live.
🖼️ [Image] — The Allowed domains editor with the "Require strict origin" toggle beneath it.
Domain matching rules
| Entry | Matches | Does not match |
|---|---|---|
example.com | example.com on any port | www.example.com, app.example.com |
*.example.com | www.example.com, app.example.com, any subdomain | the bare example.com |
localhost | localhost, 127.0.0.1, ::1 — on any port | a public hostname |
localhost:3000 | only that port | localhost:5173 |
Two rules catch people out. A wildcard does not include the bare domain, so a site serving both example.com and www.example.com needs both entries (or example.com plus *.example.com). And localhost, 127.0.0.1, and ::1 are treated as the same host, so one entry covers all three in local development.
How a request is judged
- The
Originheader is checked first. Browsers set it themselves and a page cannot forge it, which makes it the only fully trustworthy signal. - A parent-origin header sent by the embedded widget is the first fallback, for the cases where a browser omits
Origin. - The
Refererheader is the last fallback, and only when it is anhttp/httpsURL.
Require strict origin rejects both fallbacks and accepts only a genuine browser Origin. It is the strongest setting and the right one once your widget is running on known domains — the fallbacks exist for compatibility, and anything a non-browser client can set, a non-browser client can fake.
With a non-empty list, a request carrying no origin information at all is refused. You'll see one of these:
| Response | Meaning |
|---|---|
403 ORIGIN_REQUIRED | The allow-list is set but the request proved no origin |
403 DOMAIN_NOT_ALLOWED | The origin was proved and is not on your list |
401 TENANT_NOT_RESOLVED | The request didn't identify a workspace — usually a wrong or missing API key |
The guard applies to widget endpoints. Dashboard and admin access is protected by authentication, not by origin.
Pair the allow-list with your API key: the key identifies your workspace, the allow-list constrains where it may be used. Setup detail is in Embedding the widget.
Rate limiting
Promptly counts requests in fixed one-minute windows and refuses what exceeds the limit. Widget traffic is counted per visitor session and per IP; dashboard traffic per workspace; authentication attempts per IP.
| What | Limit |
|---|---|
| Widget chat messages | 20 per minute per visitor, with a per-IP ceiling behind it |
| Starting a conversation | 10 per minute per visitor |
| Widget configuration fetches | 60 per minute per visitor |
| Dashboard / tenant API | 300 per minute per workspace |
| Sign-in and registration | 30 per minute per IP |
| Self-service sign-up | 3 per 15 minutes per IP |
| Password reset requests | 10 per 15 minutes per IP |
The two-layer counting for widget traffic exists because of shared IP addresses. A single office, school, or mobile carrier can put hundreds of genuine visitors behind one IP; counting only per IP would throttle all of them because of one. So the tight limit is per session, and the per-IP number sits well above it as a backstop against a script cycling through sessions.
When you hit a limit you get 429 Too Many Requests with a Retry-After header saying when to try again. The widget handles this on its own — a visitor typing quickly sees a brief pause, not an error.
Sustained abuse is different. Ordinary rate-limit hits are not held against you. But behaviour with no legitimate explanation — probing many workspaces, tripping security guards repeatedly — leads to a temporary IP ban: 403 for an hour. Bans survive a restart, and the Promptly team can lift one.
You cannot configure these limits, and they apply to every workspace including yours. If a legitimate integration of yours needs a higher ceiling, talk to the Promptly team rather than working around it — a retry loop hammering a 429 is exactly the pattern the ban logic looks for.
A practical hardening checklist
- Add every domain your widget runs on, including staging.
- Remove
localhostentries when you're done developing. - Turn on Require strict origin.
- Confirm the widget still loads on your site — then treat the list as part of your deployment checklist, so a new domain gets added before it goes live.