Quick start: chat in your website or web app (signed-in users)
About 15 minutes. You add one endpoint to your server and one snippet to your page. Gauge then knows which user is chatting and which Integration they are on.
You need: your App id, your identity secret and the Integration key (Console → Apps → your App → Install), plus Node ≥ 18 or Python ≥ 3.9. Using Next.js, Django, Laravel, Rails or Go? Each has a full page, webhook included: quickstarts/.
1. Your server: an endpoint that returns a token
Node / Express:
import { mintIntegrationToken } from '@gauge/support';
// GET /gauge-token: your session middleware has already set req.user
export async function gaugeToken(req, res) {
const token = await mintIntegrationToken(process.env.GAUGE_IDENTITY_SECRET, {
appId: process.env.GAUGE_APP_ID,
userId: req.user.id,
integration: 'customer',
name: req.user.name,
email: req.user.email,
});
res.json({ token });
}Python / Flask:
import os
from flask import jsonify, g
from gauge_support import mint_integration_token
def gauge_token(): # GET /gauge-token, behind your login
token = mint_integration_token(
os.environ["GAUGE_IDENTITY_SECRET"],
app_id=os.environ["GAUGE_APP_ID"],
user_id=g.user.id,
integration="customer",
)
return jsonify(token=token)2. Your page: the widget
List your site first. Every site the widget runs on must be listed in the App's allowed websites
(Apps → your App → Install → Allowed sites), as scheme://host[:port], for example
https://app.example.com or http://localhost:3000. If it is not listed, the browser blocks the widget:
Gauge sends no CORS headers to that page, so the chat cannot open or send. Matching ignores case and is
exact on scheme, host and port. A port, http instead of https, or a subdomain each count as a
separate site. List one origin for every port you develop on. Changes apply within about a minute.
<script src="https://widget.gauge.chat/v1/<version>/widget.js"
integrity="sha384-<integrity>" crossorigin="anonymous" async></script>
<script>
window.addEventListener('load', function () {
window.PulseWebchat.init({
apiUrl: 'https://app.gauge.chat/backend',
connectionId: '<your App’s chat connection id>',
supportMode: true,
integration: 'customer',
// Called on every open, so the token is always fresh (they live 10 minutes at most).
getToken: function () {
return fetch('/gauge-token', { credentials: 'include' })
.then(function (r) { return r.json(); })
.then(function (j) { return j.token; });
},
});
});
</script>CSP: script-src https://widget.gauge.chat and connect-src https://app.gauge.chat wss://app.gauge.chat.
Logged-out visitors: leave out getToken. They chat anonymously when the integration allows it.
Sign-out and shared browsers (single-page apps: required)
Call PulseWebchat.logout() from your own sign-out, before you clear your session. It ends the
user's live chat, expires the widget's visitor cookie and removes the widget from the page. Then call
init(...) again for whoever comes next (signed in, or anonymous without getToken):
async function signOut() {
await window.PulseWebchat.logout(); // with the npm SDK: `await reset()` from '@gauge/support/browser'
await fetch('/logout', { method: 'POST', credentials: 'include' });
window.PulseWebchat.init({ apiUrl: 'https://app.gauge.chat/backend', connectionId: '<id>', supportMode: true });
}The server also protects you if you forget: a chat resumes only for the same person (token sub or
hash externalId) at the same identity level. Anyone else on that browser, including an anonymous
visitor after a signed-in one, gets a new chat and a new cookie, and the previous chat ends.
logout() is still the right call, because it ends the chat at sign-out rather than when the next
person happens to open the widget.
When the same person comes back to a chat that is still open, the widget reopens that same conversation, with its history and whoever was already handling it.
Their chats: the home view
A visitor who has chatted before lands on a home view when the panel opens: your greeting, a Start new chat key, and their chats, newest first. Each row shows who handled it (the agent's photo or initials, the assistant's mark for an AI chat, or a team mark while it waits), the topic or the visitor's first line, the last message, the time, the state (Open, Waiting, Closed) and how many replies they have not read. A visitor with no chats goes straight into a new chat with your greeting and starter topics.
- Closed chats are read-only. Opening one shows its history, a line saying when and why it closed, and Start new chat in place of the message box. A new chat is a new conversation; the old one stays closed (your team can still reopen it from the console).
- One open chat at a time. While the visitor has an open chat, the home key reads Continue chat and takes them back into it.
- Unread. When an agent or the assistant replies, the floating launcher shows the visitor's total
unread count, each row shows its own, and embedded mode reports the total in
onStateChange. Opening a chat marks it read, and a reply that arrives while the visitor is looking at it never counts. - Where it opens.
openTo: 'home'(default) shows the list;openTo: 'last-open'goes straight back into the visitor's open chat when they have one. Your App's Appearance → When the widget opens sets the same choice from the console; theinitoption wins over it.
Only the visitor's own chats are listed: a signed-in user sees theirs from any device, and an anonymous visitor sees the chats from this browser only.
Embedded in your own panel
If your app already has a chat drawer with its own header and close button, render the conversation inside it instead of the floating launcher:
<aside id="help-drawer" hidden>
<header><strong id="help-title">Support</strong> <span id="help-badge"></span>
<button id="help-close" aria-label="Close">×</button></header>
<div id="help-chat" style="height: 520px"></div>
</aside>
<script>
const chat = window.PulseWebchat.init({
apiUrl: 'https://app.gauge.chat/backend',
connectionId: '<your App’s chat connection id>',
supportMode: true,
getToken: () => fetch('/gauge-token', { credentials: 'include' }).then((r) => r.json()).then((j) => j.token),
mode: 'embedded',
mount: document.getElementById('help-chat'),
theme: { accent: '#1f5fbf', onAccent: '#ffffff', radius: 8 }, // #1f5fbf on white: 6.1:1
onStateChange: (s) => {
document.getElementById('help-title').textContent =
s.agentName ? s.agentName : s.status === 'offline' ? 'Support (offline)' : 'Support';
// `unread` is the visitor's unread replies across all their chats; `view` is 'home' or 'thread'.
document.getElementById('help-badge').textContent = s.unread > 0 ? String(s.unread) : '';
},
});
// Tell the widget when your drawer is shown or hidden, so `unread` counts the right replies.
function openHelp() { document.getElementById('help-drawer').hidden = false; chat.open(); }
function closeHelp() { document.getElementById('help-drawer').hidden = true; chat.close(); }
document.getElementById('help-close').onclick = closeHelp;
</script>The mount must have a height (the widget fills 100% of it and scrolls the conversation inside).
Option shapes
PulseWebchat.init(options: WebchatInitOptions): WebchatController | null
interface WebchatInitOptions extends WebchatConfig { // apiUrl, connectionId, supportMode, getToken, …
mode?: 'floating' | 'embedded'; // default 'floating' (launcher + panel), unchanged
mount?: HTMLElement; // required for 'embedded': an empty element you own
theme?: WebchatTheme;
onStateChange?: (state: WebchatState) => void;
onClose?: () => void; // floating only: the visitor closed the panel
openTo?: 'home' | 'last-open'; // default 'home'; wins over the App's Appearance setting
}
interface WebchatTheme {
accent?: string; onAccent?: string; surface?: string; ink?: string; muted?: string; // hex, rgb() or hsl()
radius?: number; // px, clamped to 0–24
fontFamily?: string; // letters, digits, spaces, commas, quotes, hyphens; omit to inherit your font
}
interface WebchatState {
status: 'connecting' | 'ready' | 'queued' | 'offline' | 'ended' | 'error';
unread: number; // the visitor's unread agent/AI replies across all their chats, less the chat on screen
view?: 'home' | 'thread'; // the chat list, or a conversation
agentName?: string; // once the server has sent one
}
interface WebchatController {
readonly mode: 'floating' | 'embedded';
open(): void; // floating: open the panel. embedded: your drawer is showing (clears unread)
close(): void; // floating: close the panel. embedded: your drawer is hidden (replies count as unread)
destroy(): void; // unmount, remove listeners, close the socket. Safe to call twice.
}mode: 'embedded' without an HTMLElement mount logs one PulseWebchat: console error, renders
nothing and returns null.
Theme rules
- Colours must be hex (
#rgb,#rrggbb),rgb()orhsl(), with no alpha. Names,var(), other functions and anything else are ignored with a console warning. - Three pairs must each reach 4.5:1 (WCAG AA):
onAccentonaccent,inkonsurface, andmutedonsurface. A missing side is measured against Gauge's default (whiteonAccent, whitesurface, near-blackink). If a pair fails, both of its colours fall back to the defaults and one console warning names the pair and the measured ratio. - The keyboard focus ring is never themed. It stays Gauge's indigo, because it is an accessibility floor and not a brand colour.
Lifecycle
init once per mount. Call controller.destroy() when your panel unmounts (React useEffect
cleanup, route change, sign-out). It closes the socket and removes every listener, so a later init
starts clean. For sign-out, PulseWebchat.logout() (above) still ends the chat on the server.
In embedded mode the widget renders no header, close button or launcher. Your drawer owns those. It does render the home view (the visitor's chats), the status line, the conversation, quick replies and the composer, with a back key from a chat to the list. When a signed-in visitor writes while nobody is online, the status line reads "No one's online right now. We'll reply here and by notification." and the composer stays open. Anonymous visitors still get the email form.
3. Check it
Open your page signed in and send a message. If the browser console shows a CORS error on
/support/chat/session, your page's origin is not in the App's allowed websites (section 2). In the console the conversation shows your user's
name and Verified. If it doesn't, paste the token from /gauge-token into Developers → Support
→ Token checker: it names the rule that failed. See integration-token.md
for every code.
Two kinds of users on one App
Mint with integration: 'salon' for your partners and 'customer' for everyone else, and grant
each user their keys with customer sync (integrationKeys). Each integration can have its own
assistant, team and hours.
Topics: starter buttons, the AI and requests
Add topics under Apps → your App → Topics (up to 10). Each has a name, a description the AI reads, an Answered by (who handles it), an AI behaviour and up to 3 details to collect:
- Up to 4 topics per integration appear as starter buttons under the widget's greeting. Nothing to change in your page: the widget draws them.
- A typed first message is labelled by the AI (cheapest model; skipped with no AI credit; low confidence stays "Other"). A starter tap or an agent's choice is never overwritten.
- AI behaviour: Answer (default), Answer + offer a person (a "Talk to a person" quick reply), Hand to a person (no AI answer, straight to the topic's team), Open a request (the AI collects the details, asks the customer to confirm, then opens a ticket and gives the reference).
- Details: a detail with Fill from app attribute
context_idis filled from the token you mint (addattrs: { context_type: 'booking', context_id: 'BK-1042' }), so the AI never asks for it. - Quick replies (≤3 buttons) show under the AI's latest message; a tap sends its words.
Native screens use the same list: GET /v1/support/projects/:id/topics (see api-s1.md §7.2a).
Gauge Support quick start
Wire your SaaS to Gauge Support in about 15 minutes: provision your users, embed the chat widget (or sign users into the portal), and receive verified webhooks.
Quick start: chat in your mobile app (WebView)
Your backend mints a one-time sign-in link for the signed-in user and the Integration. Your app opens it in a WebView, which lands in that integration's live chat. No secret ever ships in the app.