Skip to main content

JavaScript

The script tag on its own is already a working installation. Loading it from your own code is what lets you tell the widget who is signed in, open it from a button of your own, and keep the key in one module instead of in every page.

A loader you can call

One module owns the tag and the key. Your own key is on the Chat widget screen of your workspace; it starts with pk_ and is meant to be public — it names a project and grants nothing beyond starting a conversation with it.

javascript
// support.js — an ES module, no build step required.
const SRC = 'https://api.helpwing.app/widget.js'
const PROJECT_KEY = 'pk_your_project_key'

/**
 * Adds the script tag. Safe to call more than once: the second call returns
 * before touching the document.
 */
export function loadSupportWidget() {
  if (document.querySelector('script[data-project]')) return

  // Seed the queue before the tag, not after. The script is async, so
  // anything your app calls in the meantime has to land somewhere.
  window.Support ??= { q: [] }

  const script = document.createElement('script')
  script.async = true
  script.src = SRC
  script.dataset.project = PROJECT_KEY
  document.head.appendChild(script)
}

Why the queue exists

The tag is async, so window.Support almost never exists yet when your app first wants it. Waiting on onload would mean holding your own code back for a script that is deliberately off the critical path. Push instead: the widget drains window.Support.q — an array of [method, argument] pairs — the moment it boots, in order, so nothing is lost and nothing has to be retried.

javascript
// The five methods, each safe to call at any moment.
function invoke(method, payload) {
  const support = window.Support
  if (!support) return

  // Once the widget has booted, window.Support is the real API and `q` is
  // gone. Until then it is the stub above and the call is replayed on load.
  if (typeof support[method] === 'function') support[method](payload)
  else support.q?.push([method, payload])
}

export const support = {
  identify: identity => invoke('identify', identity),
  open: () => invoke('open'),
  close: () => invoke('close'),
  show: () => invoke('show'),
  hide: () => invoke('hide'),
}

Both branches matter. Once the script has booted it replaces the stub outright, and pushing to q at that point would put the call somewhere nothing reads it again.

Putting it together

javascript
import { loadSupportWidget, support } from './support.js'

// Last, after your own UI. Nothing on your page should wait for chat.
loadSupportWidget()

support.identify({
  id: 'usr_123',
  email: 'ada@example.com',
  name: 'Ada Lovelace',
  userHash: window.__SUPPORT_USER_HASH,   // rendered by your server
  metadata: { plan: 'pro' },
})

document
  .querySelector('#talk-to-us')
  ?.addEventListener('click', () => support.open())

Single-page routing

There is nothing to reset between routes, and nothing to tear down.

javascript
// A router that never reloads the document needs no special handling: the
// script boots once and the conversation survives every navigation.
//
// Calling identify() on each route change is fine and is the normal way to
// use it. The widget compares the payload against the last one it sent and
// only talks to the server when it actually changed.
router.afterEach(() => {
  support.identify(currentUser ? toSupportIdentity(currentUser) : null)
})
javascript
// Signing out. There is no separate reset: null is how the widget forgets
// someone. The conversation itself stays in front of the visitor.
support.identify(null)

The browser API

Everything the script puts on the page, in full:

Support.identify(identity)
Say who the visitor is, or pass null to forget them. Deduplicated: the same payload twice is one request.
Support.open()
Open the panel. Ignored until the widget has loaded its configuration.
Support.close()
Close the panel back to the launcher.
Support.show()
Show the launcher again after hide().
Support.hide()
Take the launcher off the page. The conversation underneath is kept.

The identity object

id: string
Your own user id. Required, and at most 120 characters.
email: string
Optional. What an agent replies to when the chat becomes a ticket.
name: string
Optional, at most 150 characters.
userHash: string
Hex HMAC-SHA256 of id, computed on your server. Without it the identity is unproved.
metadata: object
Optional flat map of strings, numbers and booleans, shown beside the conversation.

userHash is what makes the claim trustworthy, and only your server can produce it — Identifying users covers how, and what changes when you switch verification on.

If you send a Content-Security-Policy

Whichever host serves widget.js is also the host it calls: the script derives its API base from its own src, so one origin covers both directives. The widget renders into a shadow root with a stylesheet of its own, and shadow DOM does not exempt it from your policy.

http
Content-Security-Policy:
  script-src  'self' https://api.helpwing.app;
  connect-src 'self' https://api.helpwing.app;
  img-src     'self' https://api.helpwing.app data:;
  style-src   'self' 'unsafe-inline';

img-src is for the avatar on your project and for image attachments in a conversation. If your policy already allows inline styles you have nothing to change there.

Things worth knowing

  • It boots once per page. A second tag is refused outright — but it is still downloaded and parsed, so guard your loader rather than relying on it.
  • Browser only. There is no document while your server renders, and an identity belongs to a request rather than to a render cache.
  • The visitor is remembered in localStorage. Not in a cookie, and no cookies are sent with its requests. Private browsing degrades to a conversation that lasts the page view.
  • Allowed origins include your dev host. Add every host you serve from under Chat widget → Security. A blocked origin fails silently on purpose — the widget stays invisible rather than breaking the page it is on.
  • Nothing throws at your users. A wrong key, a blocked origin or no network leaves the page exactly as it was.
NextReact →A provider, a hook, and identity that follows the user your app already knows.