Connectly (opens in a new tab)

Connectly Webchat

A floating launcher and a chat panel you add to any page. The visitor types, the conversation lands in your Connectly inbox. One script tag, or one import.

The whole integration

<script defer src="https://webchat.connectly.ai/min.js"></script>
<script type="module">
  window.ConnectlyWebchat?.init({
    clientKey: '<your-client-key>',
  });
</script>

Customize the selected canvas Documentation Open in settings (opens in a new tab)

Install Configure API Troubleshooting ↑ Top

Connectly Webchat

A floating launcher and a chat panel you add to any page. A visitor clicks the launcher, types, and the conversation lands in your Connectly inbox alongside every other channel. There is no page to build and no UI to write.

What you need

One thing: a client key, from the webchat settings page in your Connectly dashboard. Everything else — colours, the launcher's side and icon, the panel's size, the greeting — is stored against that key and fetched at runtime, which is why a working integration is two lines rather than a configuration file.

The key goes in your page's HTML where anyone can read it. That is fine — it names your business, it does not authorise anything.

What it is not

It is not an iframe you size yourself, and it is not a component library. The widget lives in a shadow root and is position: fixed to a viewport corner, so it does not inherit your page's CSS and does not participate in your layout. Where you put the element in the DOM does not matter.

It is also not something you should load twice — see "One integration per page" on the next page. Two integrations means two launchers, two sessions and two sockets for one visitor, and nothing errors.

Install

Two paths, decided by a single question: does your site have an npm install step?

  • No build step — a CMS, a page builder, a static site, a tag manager, anywhere you can paste HTML — use the script tag.
  • Bundling your own frontend — React, Vue, Svelte, Solid, Angular — use the npm package. It ships ES modules and a typed React wrapper.

The script tag

Paste this before the closing </body> tag. <head> also works — the script defers itself until the page is ready either way.

<script defer src="https://webchat.connectly.ai/min.js"></script>
<connectly-webchat client-key="<your-client-key>"></connectly-webchat>

That is the whole integration. The launcher appears in the corner once the widget loads.

Place the element; do not call a function. <connectly-webchat> is a custom element, and the browser upgrades whatever elements are already on the page the moment the definition lands. Your markup may therefore sit before or after the script tag with the same result, and this form works where a CMS, a theme or a tag manager lets you add markup but not run a script.

If you do need the JS API, keep the call in a second tag carrying defer or type="module", after the first. Both attributes hold execution until the document has parsed and then run in document order, so window.ConnectlyWebchat exists by the time the second tag runs; a plain inline <script> with neither attribute runs first, against a global that is not there yet. The sturdier form — and the one to use when you cannot control tag order — is the ready event, which you may subscribe to before the widget exists because it bubbles to document:

<script defer src="https://webchat.connectly.ai/min.js"></script>
<connectly-webchat client-key="<your-client-key>"></connectly-webchat>
<script>
  // A plain inline script is fine HERE: the listener is registered before the deferred
  // bundle runs, which is exactly the ordering problem the ready event solves.
  document.addEventListener('connectly-webchat:ready', (event) => {
    console.log('webchat ready', event.detail);
  });
</script>

open() is deliberately not what this example calls: it would pop the panel up for every visitor on every page. Use the event to hook up analytics, and leave opening to the reader.

The event fires when the launcher renders, and is not replayed — register the listener before the widget loads rather than after. It fires again if the widget is torn down and re-initialized (a changed client-key, or an SPA remount), so it is once per launcher rather than once per page.

The npm package

npm i git+https://github.com/connectlyai/webchat.git#v1.1.0

A git URL rather than a registry name: the package is distributed from its own repository, github.com/connectlyai/webchat, which npm, pnpm and yarn all install from natively.

Always pin a tag. A git specifier resolves to a commit, so omit the fragment after # and you get whatever is on the default branch at install time, which moves under you. #semver:^1.1.0 also works, resolving against the repository's tags. If the install asks for credentials, see "npm cannot install the git URL" under Troubleshooting.

ESM only, browser only. There is no CommonJS build, so reach it through import or a bundler, never require().

React

'use client'; // Next.js App Router only — see "Server rendering" below.

import { ConnectlyWebchat } from '@connectly/webchat/react';

export function App() {
  return <ConnectlyWebchat clientKey="<your-client-key>" />;
}

The component renders null. The widget is created imperatively and lives in <body>, outside your tree; unmounting the component ends the session. react and react-dom are optional peer dependencies, needed only for this entry point.

Any other framework, or none

import '@connectly/webchat';

That import is the integration: it defines the <connectly-webchat> custom element and configures the origin. Then put the element on the page.

<connectly-webchat client-key="<your-client-key>"></connectly-webchat>

Svelte, Vue, Solid and Angular all render custom elements natively, so the import plus the tag is everything — with two exceptions that break the build otherwise.

Vue needs isCustomElement, or the compiler treats the tag as a component it cannot resolve, warns on every render, and emits nothing:

vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'connectly-webchat' } } })

Angular needs schemas: [CUSTOM_ELEMENTS_SCHEMA] on the component, or the template fails with NG0304.

Where you put the element in the DOM does not matter — it is a 0×0, pointer-events: none mount point, and the launcher is fixed to the viewport corner. One exception: an ancestor with a transform, filter or contain becomes the containing block for position: fixed, so the element relocates itself to <body> — which breaks any framework that thinks it still owns that node. Append to <body> yourself, or use the React wrapper.

Server rendering

Never import the package on the server. It defines a custom element extending HTMLElement, which does not exist in a server runtime, so the import itself throws ReferenceError: HTMLElement is not defined. That includes @connectly/webchat/react, which imports the main entry.

Next.js, both routers — load it through next/dynamic with ssr: false, because even a client component is prerendered during next build:

'use client'; // App Router only; also what allows `ssr: false` here.

import dynamic from 'next/dynamic';

const ConnectlyWebchat = dynamic(
  () => import('@connectly/webchat/react').then((m) => m.ConnectlyWebchat),
  { ssr: false },
);

export function Webchat() {
  return <ConnectlyWebchat clientKey="<your-client-key>" />;
}

Everywhere else — Remix, Nuxt, Astro, SvelteKit — the rule is the same: reach it from a browser-only path (a dynamic import() in a mount or effect hook, a <ClientOnly> wrapper, client:only, a plain module <script>), never a static import at the top of a shared module.

One integration per page

Do not load the script tag and the npm package on the same page.

You get two launchers, two sessions and two sockets for one visitor, and the transcript splits unpredictably between them. Nothing errors and nothing warns — multiple widgets on one page is deliberately supported for other cases — so this is a bug you will only find by looking at it.

Configure

Most of the appearance is not configured in code. Colours, the launcher's side and icon, the panel's size and the greeting are stored against your client key in the Connectly dashboard and fetched at runtime, so the common case needs no config at all — change it in the dashboard and every page using that key follows, with no redeploy. Everything below is for what the dashboard cannot cover: a per-page override, a value driven by your own state, or a theme that has to match the page it sits on.

The precedence chain

Four layers, lowest to highest:

built-in defaults  <  dashboard settings  <  `config`  <  HTML attributes

They merge per group, not wholesale, so { launcher: { side: 'left' } } moves the launcher without resetting its size, icon and offsets to defaults.

HTML attributes get the last word. An attribute is in the page's own markup, authored by whoever owns the page; config is often set by a shared wrapper or component library that the page author cannot edit. So <connectly-webchat side="left"> wins over config={{ launcher: { side: 'right' } }}.

One consequence worth knowing: title is a global HTML attribute, so anything that sets it on the element — a CMS, a framework, a tooltip helper — overrides config.panel.title. See the title row under "Attributes" below. If you set the panel heading through config, make sure nothing is also writing a title attribute.

init() is the one place the shorthands and config are reconciled for you: the title/width/height options land as attributes, so when a single init() call passes both a shorthand and a config for the same field, the shorthand yields to config and logs a warning rather than beating it from the top tier.

<ConnectlyWebchat
  clientKey="<your-client-key>"
  config={{
    theme: { accent: '#6c2bd9', colorScheme: 'auto' },
    launcher: { side: 'left', label: 'Chat with us' },
    panel: { width: 380, height: 560 },
  }}
/>

Outside React, assigning config alone repaints nothing — it is a plain field, not an attribute, so call reload() after:

const el = document.querySelector('connectly-webchat');
el.config = { theme: { accent: '#c026d3' } };
el.reload();

Each distinct config value costs a network round-trip. Values are compared structurally, so an equal object is free — but bind config to a colour picker and you send one request per intermediate value. Memoise it, apply on commit, or for a live preview drive the --cwc-* custom properties instead (see "Theming with CSS" below).

config fields

config is a partial WidgetUi — the same shape the dashboard saves.

  • launcher — side, offsetX, offsetY, size, shape (circle | pill), label, ariaLabel, icon ({ kind: 'default' | 'url' | 'none', url })
  • panel — width, height, radius
  • theme — accent, accentText, surface, surfaceAlt, text, textMuted, border, bubbleVisitor, bubbleVisitorText, bubbleAgent, bubbleAgentText, bubbleRadius, fontFamily, colorScheme (light | dark | auto)
  • zIndex
  • language — en | es | pt (default en). The language for the widget's own built-in copy — the typing indicator, "You", the composer/send/attachment strings. Your own panel.title and panel.greeting are shown exactly as you write them, in whatever language you write them; this only affects the strings the widget supplies itself. panel.agentName (up to 40 characters) replaces the generic "Support" under agent replies and names the agent in the typing indicator ("Ana está digitando…"); the verb still follows language.
  • collision — bottomOffsetPx, maxBottomOffsetPx
  • mobile — breakpointPx, launcher (offsetX, offsetY, size), panel (fullscreen, width, height, radius — 0 means "inherit the desktop value")

Visible in autocomplete but not yet acted on, so setting them does nothing: behavior.autoOpen*, navigation.linkTarget, paths.*, panel.anchor, launcher.hideOnMobile, collision.bottomOffsetSelectors, collision.hideBehindModalDialog, position, bundleVersion.

Only the config stored for your business is range-checked. Values passed through config or an attribute are merged in as given, so panel.width: 5000 is a 5000px panel. Use the dashboard's ranges as your bounds. Note this cuts both ways with the precedence order above: an out-of-range attribute is not clamped and cannot be corrected by config.

And fontFamily names a face, it cannot load one: @font-face is ignored inside a shadow root, so declare the face in your own stylesheet and name it here.

Attributes

Every attribute is optional except client-key.

attribute meaning
client-key Required.
title The panel heading — not a tooltip, despite the attribute name. Load-bearing config: as the top precedence tier it overrides config.panel.title, so do not set it for tooltip purposes.
width, height Panel size in bare pixels: width="380". "380px" parses to NaN and is ignored. Not range-checked — see "Only the config stored for your business is range-checked" above. On desktop height is capped at the viewport in CSS (min(…, 100dvh)); width is not, and the mobile panel height is not either.
side left or right.
offset-x, offset-y Launcher offset from its corner, in pixels.
z-index, bottom-offset-px Stacking, and a flat lift above a cookie bar or a sticky cart.
open Panel open. Value-aware: open and open="" are open, open="false" and an absent attribute are closed. In JSX just write open={isOpen}.
log-level silent | error | warn | info | debug. Applies immediately, including after mount.

React props

prop reactive notes
clientKey no Changing it remounts the widget: a different key is a different business, so session, room and transcript all reset.
config yes Presentation overrides. An inline object literal is fine — see the round-trip warning above.
open yes Omit it and the widget owns its own state; pass it and you own it, and a value you never update pins the panel there. Mounting with open={true} comes up showing the panel, with no closed-then-open flash.
onOpenChange — (open: boolean) => void, fired for every open and close whoever caused it. On its own, without open, it is the read-only version: the widget stays in charge and you just observe. An inline arrow never re-mounts the widget.

log-level is not a React prop: set it with setAttribute, or write it directly in JSX.

Theming with CSS

The --cwc-* custom properties are the supported theming API. Every resolved value is written to a custom property on the host element, with no !important anywhere in that path, so yours wins:

connectly-webchat {
  --cwc-accent: #6c2bd9;
  --cwc-launcher-size: 56px;
  --cwc-panel-radius: 20px;
}

--cwc-* is desktop and --cwc-m-* is mobile, switched by a media query. Lengths take units here (56px), unlike config, which takes plain numbers. This is also the layer for anything that changes rapidly — it costs no network round-trip at all.

Reaching inside the shadow root is not supported. Class names and structure change without notice; a stylesheet that depends on them will break on an ordinary widget update.

Sign in your users

If your users log in on your site, you can tell the widget who they are. A signed-in user keeps one conversation on every browser and device, shows up in your inbox under their name and email, and the AI agent uses what you tell it about them instead of asking for it again.

It takes three pieces:

  1. An identity key, created once. Connectly keeps only its public half; the private key stays on your server.
  2. A short-lived identity token your server signs for the logged-in user.
  3. A function on the page that fetches a fresh token from your server whenever the widget asks for one.

An anonymous visitor needs none of this. A page that never passes a token gets the anonymous widget described everywhere else in these docs.

Create an identity key

As an owner of the business, open Settings → Webchat → Sign-in in Connectly and select Create key. Connectly shows two values:

value what it is
Key id wik_…. It is public, and goes in each token's kid header.
Private key A PEM. It is shown this once and Connectly does not keep it: store it in your server's secret store. Anyone holding it can sign in as any of your users.

A business has one live key at a time. To replace it, revoke it on the same tab, create a new one, and deploy the new key to your servers. Users cannot sign in between the revoke and the moment your servers sign with the new key. Like a client key, a new identity key takes up to a minute to start verifying, and a revoked one stops verifying within a minute.

Sign a token on your server

The token is a JWT signed with RS256 and your private key, with the key id in the kid header.

claim required rules
sub yes Your own id for the user: a string, or a non-negative integer, read as its digits. The same sub is the same conversation everywhere. At most 124 characters of letters, digits, ., _, - and @.
iat, exp yes The token may live at most 5 minutes: it is spent on one session start, not stored.
jti yes A fresh id for every token.
name no Shown in your inbox, and the AI agent addresses the user by it. At most 128 characters.
email no A valid email address. Shown in your inbox and to the AI agent.
attributes no Up to 10 string values about the user for the AI agent, such as { "plan": "pro" }. Names are lowercase letters, digits and _, start with a letter, and are at most 40 characters; values are at most 200 characters; an empty or null value is ignored.

Together, name, email and attributes must fit in 4 KB once encoded as JSON. A token that breaks any rule is refused, and the chat does not start. It never falls back to an anonymous visitor.

In Node, with jsonwebtoken:

import { randomUUID } from 'node:crypto';
import jwt from 'jsonwebtoken';

// PRIVATE_KEY and KEY_ID: the private key and key id shown when you created the key
// `user` is whoever is logged in on this request
const token = jwt.sign(
  { sub: String(user.id), name: user.name, email: user.email, attributes: { plan: user.plan } },
  PRIVATE_KEY,
  { algorithm: 'RS256', keyid: KEY_ID, expiresIn: '5m', jwtid: randomUUID() },
);

In Python, with PyJWT:

import time
import uuid

import jwt

# PRIVATE_KEY and KEY_ID: the private key and key id shown when you created the key
# `user` is whoever is logged in on this request
now = int(time.time())
token = jwt.encode(
    {
        "sub": str(user.id),
        "name": user.name,
        "email": user.email,
        "attributes": {"plan": user.plan},
        "iat": now,
        "exp": now + 300,
        "jti": str(uuid.uuid4()),
    },
    PRIVATE_KEY,
    algorithm="RS256",
    headers={"kid": KEY_ID},
)

Serve it from an endpoint only a logged-in user can reach, and return the token as plain text. The token passes through the user's browser, where anyone can decode it, so put nothing in it the user should not see.

Hand it to the widget

Give the widget a function that fetches a token. It calls the function each time it starts a session, so the function must fetch a new token every time rather than reuse one.

// TOKEN_URL: the endpoint above, which returns a token for the logged-in user
const getToken = async () => {
  const res = await fetch(TOKEN_URL);
  if (!res.ok) throw new Error(`webchat token: ${res.status}`);
  return res.text();
};

// script tag, from a `defer` or `type="module"` script
window.ConnectlyWebchat.init({ clientKey: '<your-client-key>', identityTokenProvider: getToken });

// or on the element itself
document.querySelector('connectly-webchat').identityTokenProvider = getToken;
// React
<ConnectlyWebchat clientKey="<your-client-key>" identityTokenProvider={getToken} />

Set it before the visitor opens the chat. Passing a new function for the same user changes nothing; passing one where there was none is a sign-in, and the widget starts over as that user. The function has 15 seconds to answer, and an error it throws stops the chat from starting rather than signing the user in anonymously.

The widget calls your function when the chat starts, and again whenever the user's session has run out (it does after an hour with no message either way, unless the chat is open in a tab in front of them), so return a new token on every call. A session that ran out starts again from your token rather than renewing itself, so it carries the user's details as they are now. The conversation stays on screen as it was.

Signing in closes the chat, and the widget sends connectly-webchat:close (in React, onOpenChange(false)). If you want it open, open it again, or pass open: true in the same init() call.

If the visitor chatted before signing in and the user has no conversation yet, that conversation becomes theirs: the same messages, now under their name in your inbox. A user who already has a conversation gets theirs, and the anonymous one stays a separate conversation in your inbox.

When you revoke the key a user signed in with, their session ends within the hour, and the widget asks your function for a new token. With your new key in place, the user carries on in the same conversation without noticing.

Signing out

When your page logs the user out, tell the widget, so the next person on that browser does not see their conversation:

window.ConnectlyWebchat.logout();
// or
document.querySelector('connectly-webchat').logout();

In React, stop passing identityTokenProvider.

The chat closes, as it does on a sign-in, and the next visitor on that browser starts a new anonymous conversation, not the one from before the sign-in.

A signed-in conversation is never written to the browser's storage: it lives in the page, and the widget signs the user in again on the next page load.

What happens to the details

  • Your inbox shows the token's name and adds its email to the contact. If someone on your team renames the contact, the rename sticks; later tokens do not overwrite it.
  • The AI agent sees the name, email and attributes your latest token gave, never an edit made in the inbox. A token without a name keeps the name an earlier token gave.

API

Which API you get depends on which integration you chose.

  • The script tag publishes a page global, window.ConnectlyWebchat.
  • The npm package publishes none — the element itself is the API. Reach it from outside your component tree with document.querySelector('connectly-webchat').

window.ConnectlyWebchat — script tag only

The global is published when the widget bundle executes, so it exists in any tag that runs after the bundle's — a defer or type="module" tag — and not in a plain inline script. A plain inline script runs during parsing, and the bundle carries defer, so the inline one runs first no matter where on the page you put it. Where you cannot rely on tag order, register a connectly-webchat:ready listener on document (the event bubbles, and fires when the launcher renders — it is not replayed for a listener added later) and use the global from inside that, or place <connectly-webchat> declaratively and skip the global entirely.

window.ConnectlyWebchat: {
  init(options): Element | null;
  destroy(): void;
  open(): void;
  close(): void;
  toggle(): void;
  isOpen(): boolean;
  isMounted(): boolean;
  logout(): void;
  element(): Element | null;
  version: string;
}

init(options) accepts:

option meaning
clientKey Required.
title The panel heading.
width, height Panel size. A bare number (380) or a px string ('380px') both work; any other unit is dropped with a console warning.
logLevel silent | error | warn | info | debug.
config Presentation overrides — the same object the npm package's config prop takes.
open Open the panel as soon as it is ready, instead of waiting for a click.
onMountedChange Called with true/false as the widget is added to / removed from the page.
onOpenChange Called with true/false as the panel opens/closes. This is the one you want for a toggle button's label.
identityTokenProvider Signs your logged-in user in: a function returning a fresh identity token from your server. See Sign in your users.

Calling init() twice does not create a second widget. It returns the existing element and applies any new options to it — except clientKey, which cannot be changed on a live widget. Remove the old integration first if you need a different key.

logout() forgets a signed-in user, closes the chat, and starts a new anonymous visitor.

isMounted() asks whether the widget is on the page at all (true from shortly after init() until destroy()); isOpen() asks whether the panel is showing. isOpen() is almost always the one you want, paired with onOpenChange.

<connectly-webchat> — both integrations

The element is registered by the script tag and by the package's import alike, and it is the whole API on the npm path. See Configure for the attribute list.

const el = document.querySelector('connectly-webchat');
el.openPanel();
el.closePanel();
el.toggle();
el.open;   // boolean, reflected to the attribute
el.config = { theme: { accent: '#c026d3' } };
el.reload(); // `config` is a plain field; assigning it repaints nothing on its own
el.identityTokenProvider = getToken; // sign your logged-in user in
el.logout(); // and out again

In React, prefer the declarative open / onOpenChange props over reaching for the element — the wrapper's onOpenChange is exactly the element's :open and :close events on its own node.

Opening and closing the panel

The names differ by path — open() / close() exist only on the script tag's page global, and the element's methods are openPanel() / closePanel() — but they are equivalent: the global delegates to the element.

// React, declaratively:
<ConnectlyWebchat clientKey="<your-client-key>" open={isOpen} onOpenChange={setIsOpen} />

Closing the panel only hides it: the conversation stays connected and keeps receiving replies. While the chat is open in a tab in front of the visitor, it stays connected however long a reply takes. Otherwise, after an hour with no message either way, the widget stops renewing its session and lets the connection lapse. It reconnects, and shows whatever arrived in the meantime, as soon as the visitor sends something, comes back to the tab, or opens the chat again.

Events

Events are dispatched on the element and are bubbles / composed, so a page-level listener works without holding a reference to the element:

document.addEventListener('connectly-webchat:open', (e) => {
  console.log(e.detail);
});
event when
connectly-webchat:ready The launcher has rendered. Carries the widget version and configSource, which is default when the config had not arrived yet.
connectly-webchat:open The panel opened, whoever caused it.
connectly-webchat:close The panel closed, whoever caused it.
connectly-webchat:error Something failed hard enough to stop the conversation.
connectly-webchat:config-warning A config value was rejected or ignored.

Payloads are on event.detail — see WebchatElementEventDetail in the package's types.

Version

WEBCHAT_ELEMENT_VERSION (exported from the package, carried on the ready event, and available as window.ConnectlyWebchat.version on the script-tag path) identifies the widget code. It is the first thing Connectly support will ask for. The npm package carries its own version, on its own schedule; the two are not expected to match.

Troubleshooting

The widget fails quietly more often than it fails loudly, which is what makes these worth naming.

The launcher appears, but chat does not work — or it looks unstyled

Check the client key first. An unrecognized or revoked key makes the widget fall back to a default appearance and fail to start a conversation, without stopping the launcher from rendering. So a launcher on the page proves the script loaded; it proves nothing about the key.

Turn logging up (below) and look in the console for a warning naming the client key, or a "widget config unavailable" / "unknown or inactive" message.

A key I just created looks broken for the first minute

Expected, not a bug. A newly issued key can take up to 60 seconds to become active everywhere, and a revoked key can likewise take up to 60 seconds to actually stop working. Wait a minute and reload before concluding the integration is wrong — and do not treat a revocation as an incident response tool on its own.

npm cannot install the git URL

If HTTPS asks for credentials, the two variants that usually help:

# A developer machine set up with an SSH key rather than a token:
npm i git+ssh://git@github.com/connectlyai/webchat.git#v1.1.0

# CI, with a token in the environment and no interactive prompt available:
git config --global url."https://x-access-token:$GITHUB_TOKEN@github.com/".insteadOf "https://github.com/"

Repository not found, or a credential prompt that will not take your password, almost always means the token lacks read access to the repository — not that the tag is wrong.

Two launchers, or a transcript that splits in half

You have two integrations on one page — almost always the paste-in <script> and the npm package, one of them added by a tag manager or a theme rather than by you. Nothing errors, because multiple widgets on one page is a supported case. Search the rendered page for webchat.connectly.ai/min.js — the Connectly <script src> tag — and for any bundled import of @connectly/webchat, and remove one.

ReferenceError: HTMLElement is not defined at build or boot

The package was imported on the server. The import itself throws, before any component renders, because the module defines a custom element extending HTMLElement. This includes @connectly/webchat/react, and it includes a Next.js client component, which is still prerendered during next build. The fix is a browser-only import path — see "Server rendering" under Install.

Vue warns on every render, or Angular throws NG0304

Both mean the framework does not know <connectly-webchat> is a custom element.

  • Vue — add isCustomElement to the compiler options.
  • Angular — add CUSTOM_ELEMENTS_SCHEMA to the component's schemas.

Vue's failure is the nastier of the two: it warns rather than throwing, and emits no element at all, so the page comes up looking like the script simply did not load.

The element moved itself into <body> and my framework broke

An ancestor with a transform, filter or contain becomes the containing block for position: fixed, so the element relocates itself to <body> — and a framework that still thinks it owns that node will throw on the next update. Append the element to <body> yourself, or use the React wrapper, which expects it.

More logging

Raise the console verbosity of a production build:

<connectly-webchat client-key="<your-client-key>" log-level="debug"></connectly-webchat>
window.ConnectlyWebchat.init({ clientKey: '<your-client-key>', logLevel: 'debug' });

That separates "wrong key" from "wrong integration" from "network": you see whether the config loaded, whether a session minted, and whether the socket connected.

Support

Escalate through your usual Connectly channel with the widget version (window.ConnectlyWebchat.version, or the exported WEBCHAT_ELEMENT_VERSION) and your client key, plus a console log captured at log-level="debug".