Skip to content

Web widget

Markdown

The web widget puts the same support chat your mobile users get onto your website or web app: a launcher in the corner, a chat panel with live replies from your team or the AI agent, photo uploads, review and offer cards, and client tools. It is the same protocol as the iOS, Android, React Native and Flutter SDKs, so one inbox covers every platform and history follows a verified user everywhere.

  • One script tag or one npm package. Framework free, with a small React binding.
  • Shadow DOM. Your CSS can’t restyle the chat and ours can’t leak into your page.
  • Light on your page. About 32 KB gzip at page load and no API request until someone opens the chat. The panel code (about 8 KB) loads on first open.
  • Accessible. Keyboard first, focus trapped in the open panel, Escape closes it, new replies are announced to screen readers, and it becomes a full screen sheet on phones (under 640px).

The app key is public, so the API only answers browsers on websites you list. In the dashboard open Settings, Security, Allowed websites and add one origin per line:

https://acme.com
https://app.acme.com
https://*.acme.dev

A *. entry covers every subdomain (not the bare domain). While Development mode is on, localhost, 127.0.0.1 and [::1] work on any port, so you can build locally before you add anything. A page that is not allowed gets 403 origin_not_allowed and the widget logs a console error telling you where to add it.

Until your workspace subscribes, only 3 test devices can chat. Each browser profile counts as one device per website (a private window, another browser or another origin is a new one), and reset() registers a new one. Past the limit, opening the chat fails with 402 subscription_required.

Paste this before </body> on every page that should show the chat:

<script
src="https://cdn.jsdelivr.net/npm/@conciergesdk/[email protected]/dist/concierge.js"
data-app-key="app_YOUR_APP_KEY"
async
></script>

It configures itself from the data attributes and exposes window.Concierge:

Attribute Default What it does
data-app-key (required) Your public app key.
data-base-url https://api.conciergesdk.com API URL, for a local API in development.
data-launcher true false hides the floating button; open the chat from your own.
data-position right left puts the launcher and panel in the bottom left corner.
data-accent from the dashboard #RRGGBB accent, so the launcher has your color before the first open.
data-accent-dark data-accent #RRGGBB accent for dark mode.
data-capture-errors false true reports uncaught errors with captureError.
data-locale browser languages Any BCP 47 tag (ja, pt-BR, zh-Hant). 34 languages ship; see Localization.
data-debug false Shows the debug overlay.

Calls made before the async script finishes loading can wait in a queue. Put this above the script tag (in <head> is fine):

<script>
window.Concierge = window.Concierge || { q: [] };
Concierge.q.push(['identify', 'user_42', { token: '...' }]);
</script>

The queued calls run right after the widget configures itself. From then on window.Concierge is the SDK itself and has no q, so code that runs later calls it directly (Concierge.open()). The concierge:ready event fires on window once it is ready.

concierge.js is a few hundred bytes that import the module build next to it. If your Content Security Policy or hosting setup can’t load modules from the CDN, point the same tag at https://cdn.jsdelivr.net/npm/@conciergesdk/[email protected]/dist/concierge.bundle.js instead: the same widget as one classic file, with every language built in (about 80 KB gzip).

Terminal window
npm install @conciergesdk/web
import { Concierge } from '@conciergesdk/web';
Concierge.configure({ appKey: 'app_YOUR_APP_KEY' });
// From your Help link
document.querySelector('#help')?.addEventListener('click', () => Concierge.open());

Call configure once, in your client entry (for example src/main.ts). It shows the floating launcher, like the script tag. It takes the same settings as the data attributes: baseUrl, launcher, position, captureErrors, locale and debug, plus zIndex (stacking order, default 2147483000). For a local API in development:

Concierge.configure({ appKey: 'app_YOUR_APP_KEY', baseUrl: 'http://localhost:4000' });

configure is safe to import during server side rendering (Next.js, Remix, Astro): it does nothing until it runs in a browser.

import { ConciergeProvider, useUnreadCount, useConcierge } from '@conciergesdk/web/react';
export function App() {
return (
<ConciergeProvider appKey="app_YOUR_APP_KEY" user={session ? { id: session.userId, token: session.conciergeToken } : null}>
<Routes />
</ConciergeProvider>
);
}
export function HelpButton() {
const unread = useUnreadCount();
const concierge = useConcierge();
return <button onClick={() => concierge.open()}>Help {unread > 0 && <span>{unread}</span>}</button>;
}

session stands for your own auth state, with a token your server signed (see Identify your users). Use the provider instead of calling configure yourself: it takes the same options as props (baseUrl, launcher, locale…), configures once, identifies user (and logs out when it goes from a user to null), and renders only its children. Hooks: useUnreadCount, useChatVisible, useConciergeEnabled, useConciergeEvent, usePaywallCard, useOfferCard, useOpenScreen, useScreen.

Concierge.identify(user.id, { name: user.name, email: user.email, token });

Sign token on your server with the identity secret (see Identity verification). With a valid token the conversation follows the user across browsers and devices, mobile apps included. New apps require verified users, so an identify without a token is refused (401 identity_verification_required, the identityRejected event). Call identify on every page load while signed in, and Concierge.logout() when they sign out (never on load for signed out visitors).

// "Clear my data": wipe Concierge in this browser.
Concierge.reset();
// Hide Concierge completely (for example in a kiosk or guest mode).
Concierge.suspend();
Concierge.resume();
Concierge.isSuspended();
  • reset() wipes Concierge on this device back to a fresh install without a network request: the device token in localStorage, the identity, context, cached history and appearance, the outbox, seen cards and handled tool calls. It also forgets screens from registerScreen and the onOpenScreen handler. It closes the chat. Tools, other handlers and appearance stay, and the next open registers a new device.
  • While suspended the SDK sends no request and keeps no socket open, the chat closes and won’t open, the support button and badge are hidden, the unread count reads 0 and no events fire. Consent prompts and tool calls wait: a pending call is picked up after resume() or expires on the server.
  • resume() brings the entry points back, refreshes the unread count and sends anything queued. Suspension lasts until the page reloads, so call suspend() again on the next launch if needed.
Concierge.open();
Concierge.dismiss();
Concierge.toggle();
Concierge.setLauncherVisible(false);
const sub = Concierge.onUnreadCountChange((count) => updateBadge(count));
Concierge.getUnreadCount(); // the last known count
sub.remove(); // stop listening

The unread count stays live while the tab is visible for visitors who have chatted before, the same rule as the mobile SDKs.

Concierge.setContext({ plan: 'pro', screen: 'billing' });
Concierge.breadcrumb('clicked_upgrade');
Concierge.captureError(error);
Concierge.setMetadata({ signup: '2026-03-01' });

The team sees all of it next to the conversation. Context rides along with the next message, so nothing extra is sent while the chat is closed. configure({ captureErrors: true }) reports uncaught errors and unhandled rejections for you.

Let the AI agent or your team ask the page to do something, with the visitor’s consent:

Concierge.registerTool(
'export_invoices',
'Export the invoices as a CSV download',
{ type: 'object', properties: { year: { type: 'number' } } },
async ({ year }) => ({ file: await exportInvoices(Number(year)) }),
{ title: 'Export your invoices?' },
);

The consent prompt shows your title and always names the tool. Its text never comes from the server, and server fields can’t turn consent off. Allow runs the handler (30 second limit), Not now declines.

Register the pages support answers may link to. See Knowledge autopilot. Links must be on the page’s own origin, relative or absolute; links to other origins are refused with a console warning.

Concierge.registerScreen('settings.backup', 'Backup settings', '/settings/backup');
Concierge.unregisterScreen('settings.backup'); // its buttons stop showing

Tapping the button closes the chat and navigates with location.assign. In a single page app, route it yourself; the handler gets the screen id and the absolute same-origin URL:

Concierge.onOpenScreen((_id, url) => router.push(new URL(url).pathname));

The deep link never leaves the device: Concierge learns only the id and title, and only while the knowledge autopilot is on. When an AI answer is about a registered screen, it can end with an open screen button; the SDK shows it only if that id is registered on this device. Ids are 1 to 64 letters, digits, ., _ or -, starting with a letter; titles are 1 to 120 characters; up to 100 screens. Registering an id again replaces it, and invalid input is logged and ignored. Safe to call before configure.

Review, paywall and offer cards render in the thread with the same design as the mobile SDKs, including the offer price row. On the web:

  • Review cards open the store page in a new tab (only App Store and Google Play links are opened).
  • Paywall cards, and offers that resolve to a paywall, call your handler:
Concierge.onPaywallCard((paywallId) => router.push(`/pricing?plan=${paywallId}`));
  • Offer cards are redeemed for the web, where the server only picks paywall and comp steps from the offer’s chains (store purchases can’t run in a browser). Take over with onOfferCard to run your own checkout and pass redemption.accountToken along (RevenueCat: subscriber attribute concierge_card).

Store purchases happen in your mobile apps, so linking them to customers (refund desk, receipts) is set up there: see Link purchases to customers.

The widget follows the browser’s preferred languages. To use your site’s language instead, pass a BCP 47 tag when you configure (calling configure again with the same key changes nothing):

Concierge.configure({ appKey: 'app_YOUR_APP_KEY', locale: 'ar' });

Arabic, Hebrew and Persian lay the widget out right to left. Regions and scripts fall back sensibly (es-MX uses Spanish). The script tag takes data-locale. See Localization.

The accent, title and welcome text come from Settings, Chat appearance in the dashboard and are cached, so returning visitors see your color at once. Override locally:

Concierge.setAppearance({ accentLight: '#0A7CFF', accentDark: '#4DA3FF', title: 'Help', colorScheme: 'system' });

Light and dark follow prefers-color-scheme. Text on the accent switches to dark automatically for light accents.

Concierge.addEventListener((event) => analytics.track(`concierge_${event.type}`, event));

chatOpened, chatClosed, messageSent, messageReceived, unreadCountChanged, cardShown, cardTapped, reviewOpened, paywallOpened, offerRedeemed, offerFailed, offerUnhandled, identityRejected, availabilityChanged, error.

  • Network: nothing at page load for a first time visitor. The browser registers as a web device the first time the chat opens. Requests go out with credentials: 'omit', so your site’s cookies never reach the API.
  • Live replies: a WebSocket while the chat is open (and for returning visitors while the tab is visible), with reconnect and backoff, catching up on anything it missed.
  • Offline: messages you send offline wait in an outbox that survives reloads and go out, in order and never twice, when the connection is back. A rate limited send waits out Retry-After and reads “Sending soon”.
  • Photos: pick, paste or drop up to 4 images. They are scaled to 2048px and re-encoded as JPEG in the browser before upload.
  • Storage: the device token, identity and outbox live in localStorage, queued photos in IndexedDB, all scoped to your origin.
  • Attestation: browsers can’t attest. With device checks on, a browser registers as dev, which enforce lets through only while development mode is on.
  • Browsers: current Chrome, Edge, Firefox and Safari (Safari 15 and later).
Concierge.configure({ appKey: 'app_...', debug: true });

A draggable panel shows the connection, app key, user, customer and device ids, registered tools, pending tool calls and the last 20 requests. It logs a warning if you leave it on outside localhost.