Web widget
The web widget is an embeddable chat surface for any HTML site. The loader is 1.2 KB; the full widget bundle is 47 KB gzipped and only loaded when the user opens the chat. Built with Preact + Vite, no React peer dependency.
Install
Paste a single tag in your HTML — anywhere in <body>, but before </body> is conventional:
<script
src="https://cdn.neofir.com/loader.js"
data-org="your-org-slug"
data-api-key="pk_live_..."
async></script>
Required attributes:
data-org— your org's slug, visible in dashboard URL.data-api-key— your widget API key, found at Settings → Integrări → Widget.
Optional attributes:
data-api-url— override the backend base URL (defaulthttps://api.neofir.com/v1). Used for staging or self-hosted.
Configuration
All visual configuration (colour, welcome message, shortcuts, position, language) is set in the dashboard under Settings → Widget. The bundle reads them from the /widget/init response, not from script attributes. This keeps the embed snippet stable across rebranding.
Lifecycle
- Page loads → loader fetches widget.js asynchronously.
- Loader renders the chat bubble (no chat shell yet).
- User clicks the bubble → widget bundle loads if not already.
- Widget calls
POST /widget/initwith the visitor's persisted id (cookie-stored inneofir_vid_<slug>). /widget/initreturns org config + conversation id (new or resumed) + welcome message.- Widget renders the chat shell with the welcome message pre-populated.
- Subsequent messages POST
/widget/messageand receive an SSE stream ofdelta+tool_call+finalevents.
Visitor identity
The widget persists a visitor_id in localStorage. The same visitor across sessions resumes the same conversation if it is still open (status < 30 days). Visitor identifiers are random UUIDs — no PII is collected client-side without explicit user input.
SSE event format
Each event is a data: { ... } line followed by a blank line. Types:
{ "type": "delta", "content": "" }
{ "type": "tool_call", "tool": "check_availability", "args": { ... } }
{ "type": "final", "content": "", "chips": ["..."], "message_id": "..." }
{ "type": "error", "code": "RATE_LIMIT", "message": "..." }
CORS
Widget endpoints have open CORS (Access-Control-Allow-Origin: *). Origin restriction is enforced server-side: each org has an allowed_domains list, and the widget middleware rejects requests whose Origin header is not in the list (or whose Referer is empty, for older browsers).
Rate limits
| Endpoint | Limit | Window | Notes |
|---|---|---|---|
POST /widget/init | 10 | 5 min / IP | Prevents conversation-row spam. |
POST /widget/message | 30 | 1 min / visitor_id | Per-visitor message rate. |
| Org-wide | 20 | 1 min / org | Cross-visitor cap. |
Customization
- Primary colour, welcome message, shortcut chips (up to four) — Settings → Widget.
- Position — bottom-right (default) or bottom-left.
- Language — Romanian or English; auto-detected on first turn.
- Custom CSS — not currently supported. The widget is intentionally opinionated.
Testing
Use data-api-key with the pk_test_* prefix to point at a staging org without affecting production usage counters. The widget appearance is identical.