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:
- An identity key, created once. Connectly keeps only its public half; the private key
stays on your server.
- A short-lived identity token your server signs for the logged-in user.
- 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.
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".