Web
Chat web widget (script embed and JavaScript API)link
The New Instance Chat web widget drops a floating launcher button and a chat panel onto any website. Visitors talk to your AI assistant first, and the conversation hands off to a human agent in the dashboard when the AI cannot help or the visitor asks for a person. The widget is a single script that mounts a sandboxed <iframe>; all chat logic, realtime transport and rendering live inside that frame, so the host page stays untouched apart from one fixed-position container.
Served from https://widget.newinstance.cloud/embed.js.
The widget keylink
The widget authenticates with a publishable widget key: the key ID only (for example sk_live_abc123), copied from Dashboard → Org → Chat. It behaves like a publishable key: server-side the session is marked widget-only and clamped to the chat operation whitelist, so shipping it in page source exposes nothing else.
Never put the keyId:secretKey form in a widget. Anything inside a <script> tag is world-readable, and the secret half is for server-to-server calls only.
Script tag embed (auto-init)link
Add the script to <head> or just before </body>. Auto-init runs when data-api-key is present, on window load (immediately if the document is already complete).
<script
src="https://widget.newinstance.cloud/embed.js"
data-api-key="sk_live_abc123"
data-theme="dark"
data-position="bottom-right"
data-customer-name="Ada Lovelace"
data-customer-email="ada@example.com"
data-customer-id="usr_123"
data-auto-open="false"
></script>| Attribute | Type | Default | Description |
|---|---|---|---|
data-api-key | string, required | none | Widget key ID. Without it, auto-init does nothing. |
data-theme | dark or light | light | Widget colour scheme. Only these two values. |
data-position | bottom-right or bottom-left | bottom-right | Launcher and panel corner. |
data-transport | sse or ws | inherited | Realtime transport override, for testing. |
data-customer-token | string | none | Chat identity token from your backend. Makes the conversation belong to a verified customer; no other customer fields are needed. See Authentication and identity. |
data-theme | string | dashboard theme | dark, light, or JSON for individual tokens ({"colors":{"sentBubble":"#16a34a"}}). Ranked below the dashboard theme. See Theming. |
data-customer-name | string | none | Pre-fills customer name and skips the pre-chat form. |
data-customer-email | string | none | Pre-fills customer email. |
data-customer-id | string | none | Your internal customer ID. |
data-auto-open | "true" or absent | false | Opens the panel once the key validates. Compared strictly against the string "true". |
data-gql-endpoint | URL | derived | Self-host override for the API endpoint. |
data-widget-url | URL | derived from script src | Self-host override for the iframe origin. |
Sizing (buttonSize, panelWidth, panelHeight, zIndex) and callbacks are only available through the JavaScript API.
JavaScript API embedlink
Load the script with no data attributes, then call NInstanceChat.init(config). It returns a widget instance, or null if apiKey is missing or a widget is already mounted.
<script src="https://widget.newinstance.cloud/embed.js"></script>
<script>
const widget = NInstanceChat.init({
apiKey: 'sk_live_abc123', // key ID only, never the secret
theme: 'dark',
position: 'bottom-right',
customerName: 'Ada Lovelace',
customerEmail: 'ada@example.com',
customerId: 'usr_123',
autoOpen: false,
buttonSize: 60,
panelWidth: 400,
panelHeight: 620,
zIndex: 2147483647,
callbacks: {
onReady: () => console.log('iframe booted'),
onAuth: (info) => console.log('auth', info.valid, info.status, info.reason),
onOpen: () => console.log('panel opened'),
onClose: () => console.log('panel closed'),
onDestroy: () => console.log('widget torn down'),
},
});
document.querySelector('#help').addEventListener('click', () => widget.open());
</script>| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string, required | none | Widget key ID. |
theme | dark or light | light | Widget theme. |
transport | sse or ws | inherited from platform settings, SSE when nothing is advertised | Realtime transport override. |
customerName | string | none | Pre-fill customer name. |
customerEmail | string | none | Pre-fill customer email. |
customerId | string | none | Your internal customer ID. |
position | bottom-right or bottom-left | bottom-right | Launcher position. |
autoOpen | boolean | false | Open the panel after the key validates. |
buttonSize | number | 60 | Launcher diameter in px. |
panelWidth | number | 400 | Panel width in px. |
panelHeight | number | 620 | Panel height in px. |
zIndex | number | 2147483647 | CSS z-index of the root container. |
gqlEndpoint | URL | derived | Self-host API endpoint override. |
widgetUrl | URL | derived from script src | Self-host iframe origin override. |
callbacks | object | none | onReady, onAuth, onOpen, onClose, onDestroy. |
Everything else about the widget's behaviour (welcome and offline messages, input placeholder, file-upload, emoji and typing toggles, email-required, sound, online status, branding) is configured in the dashboard, not from the embed.
Callbacks
onReady()fires as soon as the iframe boots, before the API key round-trip. Use it to hide a host-side spinner or start a fallback timer.onAuth({ valid, status, reason })fires after key validation. The launcher stays hidden untilvalid: true.onOpen()/onClose()fire when the open or close originates inside the chat iframe (for example the panel's own close control). Opens and closes you trigger from the host (widget.open(),widget.close(), the launcher button) do not fire them; track those yourself at the call site, or pollwidget.isOpen().onDestroy()fires ondestroy()and on auth-failure cleanup.
Payloads are deliberately small and non-sensitive: lifecycle markers, sanitized status codes, public IDs. Form values, tokens and customer PII are never forwarded over this channel. A callback that throws is caught and ignored, so a host bug cannot break the widget.
Identifying the customerlink
Pass customerName, customerEmail and customerId at init() time (or as data attributes). The pre-chat info form is skipped only when a non-blank customerName was supplied; identity is seeded when customerName?.trim() is truthy. Anonymous visitors always get the form.
Those fields are display data, not authentication - they come from the browser, so the backend does not trust them. For a verified customer, pass customerToken instead; the form is skipped, and nothing else needs to be supplied:
const widget = NInstanceChat.init({
apiKey: 'sk_live_abc123',
customerToken: tokenFromYourBackend,
});The token travels in the iframe's URL fragment, which is never sent to a server, so it cannot land in the widget host's access logs. The widget reads it once and erases it from its own URL.
Local themelink
theme accepts 'dark', 'light', a partial appearance, or the legacy flat palette. It is ranked below the merchant's dashboard theme and above the built-in one, merged token by token:
NInstanceChat.init({
apiKey: 'sk_live_abc123',
theme: { mode: 'dark', colors: { sentBubble: '#16a34a' } },
});If remote configuration cannot be fetched, this local theme still applies, with the built-in palette filling the gaps. Full rules in Theming.
Errorslink
callbacks.onError receives every diagnosable problem - a rejected key, chat switched off, unreachable configuration, a rejected identity token, a dropped transport, a failed send - as { code, message, severity, recoverable, source, timestamp }. Nothing on this channel is ever rendered inside the chat interface, and it never carries the key, the token, or customer data.
NInstanceChat.init({
apiKey: 'sk_live_abc123',
callbacks: {
onError: (e) => {
if (e.code === 'INVALID_IDENTITY_TOKEN') return refreshChatToken();
if (!e.recoverable) reportToYourMonitoring(e);
},
},
});The code list is in Authentication and identity -> Diagnosing problems. WIDGET_UNREACHABLE is web-specific: the iframe never answered the loader's handshake within 10 seconds.
Set identity at init. updateConfig reloads the iframe, which resets an in-progress conversation.
Method surfacelink
widget.open(); // show the panel
widget.close(); // hide the panel
widget.toggle(); // flip open state
widget.isOpen(); // boolean, synchronous
widget.updateConfig({ theme: 'light', customerName: 'Ada' });
widget.destroy(); // tear down and remove from the DOM
// Attachments, all promise-returning
await widget.attachFile(file, 'invoice.pdf');
await widget.attachFiles([{ input: fileA }, { input: fileB, name: 'photo.png' }]);
await widget.removeAttachment(id);
await widget.clearAttachments();
const items = await widget.getAttachments();
await widget.openComposer();open,close,toggle,isOpen,destroyandupdateConfigare synchronous and run entirely in the host frame.attachFile(input, name?)accepts aFile, aBlob(a filename is generated), a data URL (data:<mime>;base64,...), or a plain base64 string (name and extension optional, MIME defaults toapplication/octet-stream). It resolves with{ success, error?, id? }; theidis the queue-local ID forremoveAttachment.attachFiles(items)returns one result per input, in the same order.getAttachments()returns metadata snapshots only, never rawFilerefs:{ id, name, type, size, status, progress, errorReason?, url? }wherestatusisqueued,uploading,uploadedorfailed.openComposer()opens the panel locally first, soisOpen()flips synchronously, then bubbles the op into the iframe.- Attached files appear in the composer strip exactly as if the customer had picked them; sending runs the normal upload and message flow.
Queueing and the 15 second timeoutlink
Every attachment and composer method is a bridged call into the iframe. Calls made before the auth handshake completes are queued in an outbox and flushed the moment onAuth reports valid: true, so you can call the API immediately after init() without racing the widget's mount.
Each bridged call has a 15 second timeout. If the iframe does not answer in time the promise rejects with '<op>' request timed out. If auth fails, queued calls reject with chat is unavailable. If you call destroy(), in-flight calls reject with widget destroyed.
Conversation modellink
Flow states run idle → collecting_info → bot_conversation → handoff_pending → live_chat → ended | error. Conversation statuses are BOT_ACTIVE, WAITING, ACTIVE, RESOLVED and CLOSED; channels are ai, live and ticket.
Conversations are real-time both ways (server-sent events or WebSocket, selected by your platform settings with SSE as the default), support file attachments, typing indicators and unread counts, and survive page reloads: session state is kept in the browser's sessionStorage, scoped to the widget. Prompts are screened by the platform's AI-injection guard before reaching the model.
The transport heartbeats roughly every 10 seconds. If no beat arrives within a 30 second window the widget tears down the transport, reconnects, and replays missed messages from the server event log, falling back to a recent-message reload if the gap is too wide. Connectivity is re-checked on online, visibilitychange and pageshow, so bfcache restores recover cleanly. Transient state surfaces to the customer as "Reconnecting" or "Offline".
Lifecycle and destroylink
- Script loads. Auto-init fires on
window load, or you callNInstanceChat.init()yourself. - The root container, hidden launcher and iframe mount. The launcher is hidden until auth succeeds.
- The iframe posts ready, then the auth result. On
valid: truethe launcher appears,autoOpenis honoured, and queued calls flush. - On auth failure the root is removed,
onDestroyfires, queued calls reject, and the console explains that chat is unavailable, disabled or not configured. - If the iframe never answers, a 10 second safety timeout removes the widget silently with a generic console warning.
A second init() while a widget is mounted returns null and warns that the widget is already initialized (it checks for #ninstance-chat-root). destroy() notifies the iframe first so it can flush in-flight network calls and close transports, then rejects pending bridged calls and removes the root.
The iframe bridge and CSPlink
The widget runs in an <iframe> loaded from the widget origin. Host and widget talk over postMessage behind two gates:
- Origin gate: the host only accepts messages whose
event.originmatches the exact origin the iframe was loaded from, and whoseevent.sourceis the iframe's owncontentWindow. Every outboundpostMessagetargets that same origin explicitly, never*. - Source-tag gate: host to widget messages are stamped
ninstance-chat-host; widget to host messages are stampedninstance-chat. Untagged messages are dropped, so a rogue script on the page cannot forge the protocol even if it wins the origin lottery.
Messages other than ready, auth and API responses are ignored until auth is validated.
Your host page CSP must allow the widget origin in both frame-src and script-src:
Content-Security-Policy: script-src 'self' https://widget.newinstance.cloud; frame-src 'self' https://widget.newinstance.cloud;
Theminglink
theme accepts dark or light only; the default is light. It can be set at embed time or changed later with updateConfig({ theme: 'light' }), which reloads the iframe. Colours, branding and copy beyond the light/dark switch are configured in the dashboard.
Troubleshootinglink
| Symptom | Cause | Fix |
|---|---|---|
| Launcher never appears | The key was rejected. | Check the console warning from onAuth. Verify you passed the key ID alone with no :secret half, and that the org is active. |
| Nothing initializes after ~10 seconds | The iframe never posted back, usually an unreachable widget origin or CSP blocking the frame. | Confirm the widget origin is reachable from the host page and allowed in frame-src and script-src. |
init() returns null, console says "already initialized" | A previous instance is still mounted. | Call destroy() first, or guard against double-init. |
| Bridged call rejects with "request timed out" | The iframe did not answer within 15 seconds. | Check the frame is still mounted and the network is up; retry after onAuth. Widget console output is prefixed [NInstance Chat], so filter DevTools on that. |
attachFile rejects with "widget destroyed" | The call was awaited across a destroy(). | Re-initialize before re-attaching, or cancel host promises on destroy. |
| Customer keeps seeing the info form | customerName is empty or whitespace. | Pass a non-blank customerName, or accept that anonymous mode requires the form. |
Conversation resets after updateConfig | updateConfig reloads the iframe by design, and only re-applies theme, customerName and customerEmail. | Set identity at init(). Use updateConfig only before a conversation starts. |