# Telloox: full integration guide This document concatenates the Telloox skill: SKILL.md followed by the reference files. Links such as reference/api.md point to the sections below, introduced by an HTML comment with the file name. # Telloox widget Telloox is a hosted live-chat widget. One ` ``` 1. Detect the stack (look for `package.json`, `next.config.*`, `nuxt.config.*`, `astro.config.*`, `svelte.config.*`, `composer.json`, `style.css` + `functions.php`, `theme.liquid`, plain `.html`). 2. Put the tag in the shared layout/template, just before `` (recipes: [reference/frameworks.md](reference/frameworks.md)). Add it exactly once: a second copy is ignored, but it wastes a request. 3. Keep `data-site` on the script element itself. The loader reads the key from that attribute. Do not rename `widget.js` or wrap the file in a bundle. 4. Do not add `type="module"`, do not self-host or proxy `widget.js`: the script must load from `https://telloox.com`, because the chat frame is served from the same origin. 5. Do not strip URL fragments (`#telloox=...`) in routers or redirects. Reply emails to visitors link to the page of the site where they chatted plus `#telloox=`; the loader reads the token, removes the fragment and reopens the conversation. When that page is not known to be on the site, the link opens the hosted chat page `https://telloox.com/c/YOUR_WIDGET_KEY` instead. ## 3. Optional configuration Full API: [reference/api.md](reference/api.md). Queue pattern, safe before the script has loaded: ```html ``` - Identify logged-in users with `identify` (name, email, up to 20 scalar custom fields). Call it once per page load, not on every render. On logout call `Telloox.push(['reset'])`, so the next person on the same browser starts as a new visitor. - Language: by default the visitor's browser language. Supported: `it`, `en`, `fr`, `de`, `es` (anything else falls back to English). Force one with `setLanguage('fr')`. - Open from your own button: `window.Telloox.open()` (use the queue form if the script may not be loaded yet). - Single-page apps need nothing extra: route changes made with the History API or hash changes are reported automatically. ## 4. Content-Security-Policy If the site sends a CSP (response header or ``), add `https://telloox.com` to `script-src` and `frame-src` (or `default-src`). Nothing else is needed. Details, nonce/`strict-dynamic` setups, Trusted Types: [reference/csp.md](reference/csp.md). Also check the workspace's **allowed domains** list (Settings -> Widget). When non-empty, the site's domain (and every staging/preview/localhost domain used for testing) must be in it. A listed domain also covers its subdomains. ## 5. Verify (do not skip) ```bash curl -s https://telloox.com/api/site/YOUR_WIDGET_KEY/status ``` ```json {"installed": false, "domain": null, "last_seen_at": null, "agents_online": false} ``` - `installed`: `true` once the widget has loaded in a real browser page and reached Telloox from an allowed domain. `curl`-ing the site's HTML does NOT count: the script must run in a browser. - `domain`: host (with `:port` if not default) of the page where the widget was last seen. Check it is the site you just changed; `installed` can already be true from an earlier install elsewhere. - `last_seen_at`: UTC ISO-8601, refreshed at most every 5 minutes per domain. - `agents_online`: whether any agent is currently available (informational). - HTTP 404 `{"error":...,"code":"404"}` means the key is unknown or the workspace is suspended. HTTP 429: more than 30 calls per minute from your IP. Procedure: 1. Deploy or serve the site with the snippet (a `file://` page never registers; use `http://localhost:PORT` or a real URL). 2. Load a page of the site once in a browser. If you have a browser tool, open the page yourself; otherwise ask the user to open it and tell you when done. 3. Poll the status endpoint every ~5 s for up to a minute until `installed` is `true` and `domain` matches. Optionally also check in the browser that the round launcher button is at the bottom-right. 4. If it stays `false`: go through [reference/troubleshooting.md](reference/troubleshooting.md). The most common causes are a wrong key, a domain missing from the allowed list, a CSP block, and the script not being on the page that was loaded. ## 6. Report to the user State concisely: which files you changed, the exact snippet placed, any identify/language/CSP changes, the verification result (`installed`, `domain`), and what remains for them (for example "answer the first chat from the panel at https://telloox.com"). Mention anything you could not verify. ## Reference files - [reference/frameworks.md](reference/frameworks.md): copy-paste recipes per stack. - [reference/api.md](reference/api.md): `window.Telloox` methods, events, limits, identify examples. - [reference/csp.md](reference/csp.md): CSP directives, allowed domains. - [reference/troubleshooting.md](reference/troubleshooting.md): symptom, cause, fix. Also privacy facts (cookies, storage, IP). --- # Framework recipes All recipes add the same tag once, site-wide. `https://telloox.com` and `YOUR_WIDGET_KEY` are the Telloox server URL and the widget key. ```html ``` How the loader finds its key: it reads `data-site` from `document.currentScript`, and falls back to the first `script[data-site][src*="widget.js"]` in the document. So the tag can be static HTML or created with `document.createElement('script')` as long as `data-site` is set (with `setAttribute`) and the `src` contains `widget.js`. A tag with no `data-site` logs `Telloox: missing data-site...` to the console and does nothing. Single-page apps: no extra code. The loader wraps `history.pushState`/`replaceState` and listens to `popstate` and `hashchange`; when the URL changes it reports the new URL and `document.title` (after 300 ms, so the router can update the title first). Install the tag once in the root layout, never per route/component. Identify examples for each stack use the queue form, which works whether or not the script has loaded: `window.Telloox = window.Telloox || []; window.Telloox.push(['identify', {...}]);` ## Plain HTML Before `` of every page (or of the shared include/partial/layout): ```html ``` ## WordPress Add to the active theme's `functions.php` (use a child theme) or to a small must-use plugin. Simplest, no script API needed: ```php add_action('wp_footer', function () { ?> true, 'strategy' => 'async']); }); add_filter('script_loader_tag', function ($tag, $handle) { return $handle === 'telloox' ? str_replace(''; }, 99); ``` If a performance plugin delays or combines JavaScript, exclude `widget.js` from it. ## Next.js, App Router `app/layout.tsx`: ```tsx import Script from 'next/script'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ``` To keep the key out of source, Vite replaces `%VITE_TELLOOX_KEY%` in `index.html` from `.env` (CRA: `%REACT_APP_TELLOOX_KEY%`): `data-site="%VITE_TELLOOX_KEY%"`. Identify after login (and when the user changes): ```jsx import { useEffect } from 'react'; export function useTellooxIdentify(user) { useEffect(() => { if (!user) return; window.Telloox = window.Telloox || []; window.Telloox.push(['identify', { name: user.name, email: user.email, plan: user.plan }]); }, [user?.email]); } ``` ## Vue 3 (Vite SPA) Same as React: tag in `index.html` before ``. Identify in the code that handles login: ```js window.Telloox = window.Telloox || []; window.Telloox.push(['identify', { name: user.name, email: user.email }]); ``` ## Nuxt 3 `nuxt.config.ts`: ```ts export default defineNuxtConfig({ app: { head: { script: [ { src: 'https://telloox.com/widget.js', 'data-site': 'YOUR_WIDGET_KEY', async: true, tagPosition: 'bodyClose' }, ], }, }, }); ``` Identify with a client-only plugin, `plugins/telloox.client.ts`: ```ts export default defineNuxtPlugin(() => { const user = useState<{ name: string; email: string } | null>('user'); // adapt to your auth store watch(user, (u) => { if (!u) return; const w = window as any; w.Telloox = w.Telloox || []; w.Telloox.push(['identify', { name: u.name, email: u.email }]); }, { immediate: true }); }); ``` ## Astro In the shared layout (for example `src/layouts/Layout.astro`), before ``. `is:inline` stops Astro from processing or bundling the tag: ```astro ``` It works with the View Transitions client router: the script runs once and URL changes are tracked through the History API. ## SvelteKit `src/app.html`: ```html
%sveltekit.body%
``` Identify in `src/routes/+layout.svelte`: ```svelte ``` ## Laravel (Blade) `config/services.php`: ```php 'telloox' => ['url' => env('TELLOOX_URL'), 'key' => env('TELLOOX_KEY')], ``` `.env`: `TELLOOX_URL=https://telloox.com` and `TELLOOX_KEY=YOUR_WIDGET_KEY`. In the main layout (`resources/views/layouts/app.blade.php`), before ``: ```blade @if (config('services.telloox.key')) @auth @endauth @endif ``` `@json` escapes `<`, `>`, `&` and quotes, so it is safe inside ` {%- if customer -%} {%- endif -%} ``` `theme.liquid` covers storefront pages. Checkout pages use their own template, so the widget does not appear there unless the plan lets you customize checkout. ## Google Tag Manager Create a **Custom HTML** tag, trigger **All Pages**, "Once per page" firing. Use the script-creating form, not a literal ` ``` This works because the loader reads `document.currentScript` (set for dynamically inserted classic scripts) and otherwise finds the element by `script[data-site][src*="widget.js"]`. Set `data-site` before appending. GTM's own script must be allowed by the site CSP, and so must `https://telloox.com` (see csp.md); with a nonce + `'strict-dynamic'` CSP, scripts inserted by trusted GTM code inherit trust. To identify a user from GTM, push values to `dataLayer` and read them in the tag, or call the queue from site code. Never interpolate GTM variables directly into JavaScript strings without JSON-encoding them. --- # JavaScript API The loader (`https://telloox.com/widget.js`) defines `window.Telloox`. Every method is wrapped so that an error never reaches the host page (it is logged with `console.warn('Telloox:', ...)`). ## Calling it before the script has loaded (queue) `async` scripts load at an unknown time. Use the queue form for calls made from page code or other scripts: ```js window.Telloox = window.Telloox || []; window.Telloox.push(['identify', { name: 'Ada', email: 'ada@example.com' }]); window.Telloox.push(['setLanguage', 'it']); window.Telloox.push(['on', 'message', function (m) { console.log(m.body); }]); ``` - Each entry is `[methodName, ...args]`. When the loader starts it replays the queue in order, then replaces `window.Telloox` with the real API object. - After loading, the same `push([name, ...args])` form keeps working, so one calling convention is safe everywhere. Unknown method names and nested `'push'` are ignored. - Always read `window.Telloox` when you call; do not keep a reference to the array you created earlier, because after load it is a different object. - Direct calls (`window.Telloox.open()`) only work after the script has run; before that `window.Telloox` is undefined or a plain array without `open`. - Commands issued before the chat frame is ready (`open`, `identify`, `setLanguage`) are buffered and applied when it is. ## Methods | Call | Effect | |---|---| | `Telloox.open()` | Open the chat panel. | | `Telloox.close()` | Close it. | | `Telloox.toggle()` | Open if closed, close if open. | | `Telloox.identify(data)` | Attach visitor details. `data` must be a plain object (see below). | | `Telloox.setLanguage(code)` | Force the widget language. `code` is a string such as `'fr'` or `'pt-BR'`. | | `Telloox.reset()` | Forget the current visitor in this browser and start over as a new one (call it on logout, see below). | | `Telloox.on(event, fn)` | Subscribe to `'open'`, `'close'` or `'message'`. | | `Telloox.push([name, ...args])` | Queue-style call of any method above. | There is no `off`/unsubscribe, no `destroy`/`hide`, and no getter. The launcher cannot be moved or hidden from the API. ### identify(data) ```js Telloox.identify({ name: 'Ada Lovelace', email: 'ada@example.com', userId: 'u_123', plan: 'pro', trial: false, seats: 5 }); ``` - `name` (string, trimmed, stored up to 120 characters). An empty string clears the name. - `email` (string, up to 190 characters). An invalid address is ignored (the previous email, if any, is kept) and the warning `Telloox: identify() ignored an invalid email` appears in the browser console; the name and custom fields of the same call are still saved. An empty string is ignored (the email cannot be cleared from the API; use `reset()` on logout). - Every other key is a custom field shown to agents next to the conversation. Limits: at most 20 fields in total (newly sent keys are kept first, older ones are dropped beyond 20), key up to 40 characters, values must be strings (up to 200 characters), numbers, booleans or `null`. Objects and arrays are skipped. `null` clears a previously sent field. New values are merged over earlier ones. - `data` must survive `JSON.stringify` (no circular structures, no functions); otherwise the call is dropped with a console warning. - Rate limit: 20 identify calls per visitor per 10 minutes. Call it once per page load or on login, not inside a render loop. - Values are supplied by the browser, so agents see them but they are not verified. Do not use them for authorization and do not send secrets or more personal data than agents need. - Giving an email also lets Telloox continue the conversation by email when the visitor is offline. ### setLanguage(code) Widget languages: `it`, `en`, `fr`, `de`, `es`. Only the first two letters of `code` matter (`'fr-CA'` is French); anything else falls back to English. Without a call, the widget uses the visitor's browser language (`navigator.language`) on the host page. The setting is per page load: call it on every page (or in the queue snippet in the shared layout). Calling it again with a different value switches the open widget. It translates the widget interface only, not agent messages. ### Events ```js Telloox.on('open', function () { /* panel opened (button, open(), toggle()) */ }); Telloox.on('close', function () { /* panel closed (button, Escape, close(), toggle()) */ }); Telloox.on('message', function (message) { /* an agent reply arrived */ }); ``` `open` and `close` have no payload and fire only when the state actually changes. `message` fires for each new agent message received while the page is open (history loaded at startup does not trigger it), also when the panel is closed. Payload: ```js { id: 1234, // message id sender: 'agent', agent_name: 'Marta', // may be null body: 'Hi! How can I help?', // text ('' for an attachment-only message) attachment: { name: 'invoice.pdf', mime: 'application/pdf', size: 48213 }, // or null/undefined created_at: '2026-01-31 10:15:00' // UTC, 'YYYY-MM-DD HH:MM:SS' } ``` Unknown event names are ignored. An exception in your handler is caught and logged. Polling pauses while the browser tab is hidden, so `message` events can arrive late when the tab is in the background. The unread badge on the launcher is automatic. ## Open the chat from your own button ```html ``` The queue form works before and after `widget.js` has loaded, and a click made too early is not lost: the chat opens as soon as it is ready. ## Identify a logged-in user: server-rendered page Render the data as JSON from the server and push it before or after the loader tag. Always encode on the server (never concatenate strings), and escape `<`, `>`, `&` for HTML. PHP: ```php ``` Jinja2 / Flask (`|tojson` escapes `<`, `>`, `&` and quotes): ```html {% if current_user.is_authenticated %} {% endif %} ``` Other stacks: see the Laravel, WordPress and Shopify examples in frameworks.md. ## Identify in a SPA after login ```js async function onLogin(user) { window.Telloox = window.Telloox || []; window.Telloox.push(['identify', { name: user.name, email: user.email, plan: user.plan }]); } ``` Call it again when the details change (for example a plan upgrade); merging means you only need to send what changed. ## Logout Call `reset()` when a user logs out of your site, so that the next person on the same browser does not see the previous conversation: ```js function onLogout() { window.Telloox = window.Telloox || []; window.Telloox.push(['reset']); } ``` - The visitor is recognised by a token the chat frame keeps in its own localStorage (key `telloox:` on the Telloox origin), which the host page cannot read or delete. `reset()` drops that token, empties the chat window and starts a brand-new visitor without name, email, custom fields or history. The launcher badge goes back to zero; the panel stays open or closed as it was. - The previous visitor and their conversations stay in the Telloox panel (agents see that visitor leave). Nothing is deleted. - Calls queued after `reset` (for example `identify` for the next user) apply to the new visitor. - If logout navigates to another page right away, the call can be lost with the old page. Then push `reset` on the page the logout lands on instead, once (for example when your server flags a fresh logout), not on every page load: each call starts another new visitor. ## Navigation and SPA tracking The loader reports the page URL and title on load and again after every `history.pushState`, `history.replaceState`, `popstate` and `hashchange` that changes `location.href`. No API call is needed. ## Not supported Positioning, theming and welcome text are set in the panel (Settings -> Widget: accent colour, welcome message, privacy link, allowed domains), not in code. There are no `data-*` options on the script tag other than `data-site`. --- # Content-Security-Policy and allowed domains ## What the host site must allow The loader is a classic script that creates a closed shadow root on the host page and one `