NewInstance Documentation
View as Markdown

Web

The NewInstance 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.

Authenticationlink

The widget authenticates in exactly one of two modes (see Authentication and identity):

  • Customer token (recommended). Your server mints a token with POST /api/v1/chat/sessions and renders it into the page (data-customer-token) or returns it to your front end (customerToken). No key appears in the page at all.
  • Key mode (trusted internal pages only). data-api-key or apiKey set to the full keyId:secret. Anything inside a <script> tag is world-readable, so this puts your secret in front of every visitor: use it only on internal pages behind your own sign-in, never on a public website. The full key travels to the widget iframe in the URL fragment, never the query string.

Pass one or the other. Passing both, or neither, is a configuration error (E_CONFIG).

Deprecated: data-api-key (or apiKey) with only the key ID, such as sk_live_abc123, runs the pre-0.4.0 behaviour unchanged, including anonymous visitors, so deployed embeds keep working. It logs one console warning per page load. Do not use it for new embeds.

Script tag embed (auto-init)link

Add the script to <head> or just before </body>. Auto-init runs when data-customer-token or data-api-key is present, on window load (immediately if the document is already complete).

<script
  src="https://widget.newinstance.cloud/embed.js"
  data-customer-token="TOKEN_FROM_YOUR_BACKEND"
  data-theme="dark"
  data-position="bottom-right"
  data-auto-open="false"
></script>
AttributeTypeDefaultDescription
data-customer-tokenstringnoneCustomer token minted by your server. Selects customer token mode (recommended); no customer fields are needed. See Authentication and identity.
data-api-keystringnoneThe full keyId:secret selects key mode, for trusted internal pages only. A bare key ID is the deprecated legacy form. Pass this or data-customer-token, not both.
data-themedark or lightlightWidget colour scheme. Only these two values.
data-positionbottom-right or bottom-leftbottom-rightLauncher and panel corner.
data-transportsse or wsinheritedRealtime transport override, for testing.
data-themestringdashboard themedark, light, or JSON for individual tokens ({"colors":{"sentBubble":"#16a34a"}}). Ranked below the dashboard theme. See Theming.
data-customer-name / data-customer-email / data-customer-idstringnoneDeprecated bare key ID embeds only, where they pre-fill untrusted display data. In token mode the identity comes from the token; in key mode call setUser.
data-auto-open"true" or absentfalseOpens the panel once the key validates. Compared strictly against the string "true".
data-gql-endpointURLderivedSelf-host override for the API endpoint.
data-widget-urlURLderived from script srcSelf-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 a widget is already mounted.

<script src="https://widget.newinstance.cloud/embed.js"></script>
<script>
  const widget = NInstanceChat.init({
    customerToken: tokenFromYourBackend,
    theme: 'dark',
    position: 'bottom-right',
    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>
OptionTypeDefaultDescription
customerTokenstringnoneCustomer token minted by your server. Selects customer token mode (recommended).
apiKeystringnoneThe full keyId:secret. Selects key mode, for trusted internal pages only. Pass customerToken or apiKey, not both. A bare key ID is the deprecated legacy form.
themedark or lightlightWidget theme.
transportsse or wsinherited from platform settings, SSE when nothing is advertisedRealtime transport override.
customerName / customerEmail / customerIdstringnoneDeprecated bare key ID embeds only. Use the token, or setUser in key mode.
positionbottom-right or bottom-leftbottom-rightLauncher position.
autoOpenbooleanfalseOpen the panel after the key validates.
buttonSizenumber60Launcher diameter in px.
panelWidthnumber400Panel width in px.
panelHeightnumber620Panel height in px.
zIndexnumber2147483647CSS z-index of the root container.
gqlEndpointURLderivedSelf-host API endpoint override.
widgetUrlURLderived from script srcSelf-host iframe origin override.
callbacksobjectnoneonReady, 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.

Callbackslink

  • 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 credential validation. The launcher stays hidden until valid: 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 poll widget.isOpen().
  • onDestroy() fires on destroy() 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

In customer token mode the token is the identity: the pre-chat form is skipped and nothing else needs to be supplied. When a fresh token is due, swap it in without reloading the chat:

const widget = NInstanceChat.init({ customerToken: tokenFromYourBackend });

widget.setUserToken(freshTokenFromYourBackend);

setUserToken posts the token to the widget iframe, targeted at the widget origin and never *. The iframe is not reloaded and the conversation carries on.

In key mode, name the customer with setUser on the instance init returns:

const internal = NInstanceChat.init({ apiKey: 'sk_live_abc123:YOUR_SECRET_KEY' });
internal.setUser({ customerName: 'Ada Lovelace', customerEmail: 'ada@example.com', customerId: 'usr_123' });

The token, or the full key in key mode, 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.

The customerName, customerEmail and customerId init options belong to the deprecated bare key ID form. There they are display data, not authentication, and the pre-chat form is skipped only when a non-blank customerName was supplied; anonymous visitors get the form.

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({
  customerToken: tokenFromYourBackend,
  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({
  customerToken: tokenFromYourBackend,
  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, then use setUserToken (customer token mode) or setUser (key mode). 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.setUserToken(freshTokenFromYourBackend);
widget.setUser({ customerName: 'Ada Lovelace' });
widget.updateConfig({ theme: 'light' });
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, destroy and updateConfig are synchronous and run entirely in the host frame.
  • setUserToken(token) swaps the customer token without reloading the iframe; it is a configuration error in key mode. setUser(user) names the customer in key mode; in customer token mode its fields are not sent.
  • attachFile(input, name?) accepts a File, a Blob (a filename is generated), a data URL (data:<mime>;base64,...), or a plain base64 string (name and extension optional, MIME defaults to application/octet-stream). It resolves with { success, error?, id? }; the id is the queue-local ID for removeAttachment.
  • attachFiles(items) returns one result per input, in the same order.
  • getAttachments() returns metadata snapshots only, never raw File refs: { id, name, type, size, status, progress, errorReason?, url? } where status is queued, uploading, uploaded or failed.
  • openComposer() opens the panel locally first, so isOpen() 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

  1. Script loads. Auto-init fires on window load, or you call NInstanceChat.init() yourself.
  2. The root container, hidden launcher and iframe mount. The launcher is hidden until auth succeeds.
  3. The iframe posts ready, then the auth result. On valid: true the launcher appears, autoOpen is honoured, and queued calls flush.
  4. On auth failure the root is removed, onDestroy fires, queued calls reject, and the console explains that chat is unavailable, disabled or not configured.
  5. 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.origin matches the exact origin the iframe was loaded from, and whose event.source is the iframe's own contentWindow. Every outbound postMessage targets that same origin explicitly, never *.
  • Source-tag gate: host to widget messages are stamped ninstance-chat-host; widget to host messages are stamped ninstance-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

SymptomCauseFix
Launcher never appearsThe token or key was rejected.Check the console warning from onAuth. In token mode, mint a fresh token; in key mode, pass the full keyId:secret. Confirm the key and the org are active.
Nothing initializes after ~10 secondsThe 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 formNo identity reached the widget.Pass a customer token, or call setUser with a non-blank customerName in key mode. Deprecated bare key ID embeds show the form to anonymous visitors.
Conversation resets after updateConfigupdateConfig reloads the iframe by design, and only re-applies theme, customerName and customerEmail.Set identity at init(). Swap tokens with setUserToken, which does not reload. Use updateConfig only before a conversation starts.
Console warns that the bare key ID form is deprecatedThe embed passes data-api-key or apiKey with only the key ID.It keeps working. Move public pages to a customer token (data-customer-token).