Widget JavaScript API
Complete reference for the Convor widget browser API: route synchronization, visitor prefill, verified identity, metadata, events, and programmatic chat control.
Widget JavaScript API
The canonical widget loader exposes one browser API:
window.ConvorConvor controls the visible widget, iframe, and visitor session. No second
widget global or parallel SDK instance is created.
Load the API
The dashboard-generated production embed loads the widget and exposes the API automatically:
<script
src="https://cdn.convor.io/widget.js"
data-key="convor_wpk_..."
async
></script>data-key is the public widget key copied from the dashboard. It starts with
convor_wpk_, is expected to be visible in browser code, and is not an
organization secret API key. The hosted bundle already contains the production
service endpoints; do not configure API, iframe, or realtime URLs in browser
code.
Wait for the visitor session before sending visitor or message commands:
const state = await Convor.ready();
console.log(state.visitorId);Load widget.js once. Convor.init(options) is available when an application
intentionally initializes the widget itself, but the normal embed does not need
an additional SDK script.
Programmatic loading
To prevent automatic initialization, load widget.js without data-key, then
call Convor.init() after the script has loaded:
<script id="convor-loader" src="https://cdn.convor.io/widget.js" async></script>
<script>
document.getElementById("convor-loader").addEventListener("load", async () => {
await Convor.init({
key: "convor_wpk_...",
locale: document.documentElement.lang,
});
});
</script>Do not add data-key to the loader when you want programmatic initialization;
data-key enables the normal automatic initialization path.
Multilingual SPAs
The normal embed reads the page language from <html lang> during
initialization. It does not watch later lang changes. When the application
language changes client-side, call Convor.setLocale(locale) instead of
re-initializing the widget.
setLocale() reloads the localized site configuration and built-in interface
catalog, then replaces only the iframe document. The existing launcher and host
frame stay mounted, including their open/closed state, visibility, and position.
The site-scoped visitor identity is preserved.
"use client";
import Script from "next/script";
import {useEffect, useRef, useState} from "react";
export function ConvorWidget({locale}: {locale: string}) {
const [loaded, setLoaded] = useState(false);
const lastLocale = useRef(locale);
useEffect(() => {
if (!loaded || !window.Convor || lastLocale.current === locale) return;
lastLocale.current = locale;
void window.Convor.setLocale(locale);
}, [loaded, locale]);
return (
<Script
src="https://cdn.convor.io/widget.js"
data-key="convor_wpk_..."
strategy="afterInteractive"
onLoad={() => setLoaded(true)}
/>
);
}Site content languages and Convor-owned interface catalogs are independent. A requested locale can select matching visitor-facing content whenever that locale is enabled for the site, even if the widget interface needs a fallback catalog. Convor currently ships English and Polish interface catalogs; additional interface languages can be released without changing a site's content-language configuration.
For example, Convor.setLocale("cs") can select Czech site content while the
built-in controls use the closest interface catalog available from Convor.
For route changes that do not update the browser URL, call Convor.setPage()
separately; setPage() does not change the widget language.
Common integration
await Convor.ready();
await Convor.setUser({
name: currentUser.name,
email: currentUser.email,
phone: currentUser.phone,
attributes: {
customerId: {label: "Customer ID", value: currentUser.id},
currency: {label: "Currency", value: currentUser.currency},
dropShipping: currentUser.isDropShipping,
language: {label: "Language", value: currentUser.language},
vatId: {label: "VAT ID", value: currentUser.vatId},
},
});setUser() immediately persists contact fields and attributes to the visitor
profile shown in the Convor dashboard. It also prefills matching pre-chat form
fields.
Trusted and self-reported identity
Use setUser() for convenient browser-side prefill and metadata. Use
identify() with a server-generated HMAC when the account identity must be
trusted. Browser code can never overwrite contact fields belonging to a
verified or operator-managed profile.
Route synchronization
The loader automatically detects standard browser navigation, including
history.pushState(), history.replaceState(), popstate, and hashchange.
For routers that do not update the browser URL, send the logical route
explicitly:
await Convor.setPage({
url: "/account/orders/ORD-1042",
referrer: "/account/orders",
});Relative URLs are resolved against the current page. Only HTTP and HTTPS URLs are accepted. A successful route update:
- updates the visitor's current page and browsing history in the dashboard;
- evaluates URL-based widget triggers;
- emits
page:changeon the host page.
track("page_view") is an alias intended for analytics-style integrations:
await Convor.track("page_view", {
url: window.location.href,
referrer: document.referrer,
});Custom event names are not currently accepted by track().
Visitor data
Convor.setUser(user)
Sets self-reported contact data, application metadata, and pre-chat values. Every field is optional.
await Convor.setUser({
name: "Jane Customer",
email: "[email protected]",
phone: "+48 123 123 123",
department: "billing",
customFields: {
"company-field-id": "Acme Sp. z o.o.",
"accepted-terms-field-id": true,
},
attributes: {
customerId: {label: "Customer ID", value: 1042},
plan: {label: "Plan", value: "Scale"},
overdue: false,
},
});| Field | Type | Persistence |
|---|---|---|
name | string | null | Visitor profile and matching pre-chat field |
email | string | null | Visitor profile and matching pre-chat field |
phone | string | null | Visitor profile and matching pre-chat field |
attributes | Record<string, AttributeValue> | Visitor metadata shown in the dashboard |
department | string | null | Pre-chat department selection |
customFields | Record<string, string | number | boolean> | Configured pre-chat custom fields, keyed by custom-field ID |
Use the custom-field IDs present in the configured pre-chat form, not their display labels. Up to 50 custom fields may be supplied, and each ID is limited to 100 characters.
Passing null clears a self-reported contact value. Contact fields are ignored
when the current profile is verified or operator_entered; attributes remain
updatable because they describe application context rather than identity.
Partial pre-chat data is saved as a draft. It does not mark the form as completed and cannot bypass required fields.
Convor.setAttributes(attributes)
Updates application-defined visitor attributes without changing contact data:
await Convor.setAttributes({
plan: "team",
seats: 12,
trial: false,
customerId: {label: "Customer ID", value: "CUS-1042"},
});The aliases setVariables() and setMetadata() have the same behavior.
An attribute value can be:
string | number | boolean | {
label: string;
value: string | number | boolean;
} | nullUse a labeled value when the dashboard label should differ from the programmatic
key. Set a value to null to remove that attribute. A visitor can have at most
50 attributes; keys are limited to 100 characters, labels to 100 characters,
and string values to 2,000 characters. The prototype-control keys __proto__,
prototype, and constructor are reserved.
Convor.identify(identity)
Use identify() when your application already has an authenticated customer and
you want Convor to mark that visitor identity as verified. Generate the signing
secret in Settings → Widget → Installation and security → Verified customer identity, copy it when it is
shown, and store it only in your application's server-side secret store.
The signing flow has two trust boundaries:
- The browser obtains the current Convor visitor ID and sends that ID to your authenticated application backend.
- Your backend resolves the signed-in customer from its own session or token, signs the authoritative customer fields, and returns the signed identity to that browser session.
Do not accept arbitrary customer identity fields from the browser and sign them without checking them against your authenticated account data.
First obtain the current visitor ID in the browser:
const {visitorId} = await Convor.ready();
const identity = await fetch("/api/convor-identity", {
method: "POST",
credentials: "include",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({visitorId}),
}).then((response) => response.json());
await Convor.identify(identity);On your application server, authenticate the request, load the current customer, and compute HMAC-SHA256 over the exact UTF-8 JSON produced by the following property order. Missing optional values must be empty strings:
import {createHmac} from "node:crypto";
export async function buildConvorIdentity({visitorId, session}) {
const customer = await loadCustomerForSession(session);
const traits = {
id: customer.id,
email: customer.email ?? undefined,
name: customer.name ?? undefined,
phone: customer.phone ?? undefined,
};
const payload = JSON.stringify({
visitorId,
id: traits.id,
email: traits.email ?? "",
name: traits.name ?? "",
phone: traits.phone ?? "",
});
const userHash = createHmac(
"sha256",
process.env.CONVOR_IDENTITY_SECRET
)
.update(payload)
.digest("hex");
return {...traits, userHash};
}The exact values passed to Convor.identify() for id, email, name, and
phone must match the values used to generate the hash. userHash is a
64-character hexadecimal HMAC-SHA256 digest. Application attributes are not
part of this signature; send them with setAttributes() after identification
when needed.
The signature is also bound to the current Convor visitorId. Convor requires
the public widget key and the active visitor session to belong to the same site
and visitor before accepting the identity, so a valid hash generated for one
visitor cannot be replayed for another. Rotating or disabling the site's
identity secret stops future requests signed with the previous secret from
verifying.
Convor.getVisitorId()
Returns the current visitor UUID after ready() resolves, otherwise null:
const visitorId = Convor.getVisitorId();Widget control
Open, close, and toggle
Convor.open();
Convor.close();
Convor.toggle();openChat() and closeChat() are aliases for open() and close().
Show and hide
Convor.hide(); // Hides both launcher and panel.
Convor.show();Hiding the widget does not destroy the visitor session or disconnect the conversation.
Convor.setLocale(locale)
Changes the mounted widget language at runtime:
await Convor.setLocale("pl-PL");Pass a non-empty BCP 47-style locale such as en, de, es-MX, or
pl-PL. Convor resolves the best enabled site-content locale on the server
and selects the built-in interface catalog separately. If the site's locale
mode is Fixed, the configured default content language is used regardless
of the requested locale. The promise resolves after the new
iframe document reports that it has mounted. Commands that require the visitor
session continue to wait for the refreshed iframe session to become ready.
If the locale configuration request fails, the currently mounted widget stays unchanged. When several locale changes overlap, only the latest request is applied.
State
const state = Convor.getState();
// {
// ready: boolean,
// open: boolean,
// visible: boolean,
// unreadCount: number,
// visitorId: string | null,
// pageUrl: string
// }Convenience methods are also available:
Convor.isOpen();
Convor.isVisible();Messages
Convor.setDraft(message)
Prefills the composer without sending:
await Convor.setDraft("I need help with order ORD-1042");
Convor.open();The draft is limited to 4,000 characters and uses the normal widget draft persistence.
Convor.sendMessage(message)
Sends a visitor message through the active widget session:
const sent = await Convor.sendMessage(
"Please connect me with the billing team."
);The promise resolves with:
{
id: string;
content: string;
senderType: "visitor";
createdAt: string;
}If a pre-chat form is enabled, every configured required field must already be
completed through the form or setUser(). Programmatic sending cannot bypass
the form.
Events
Register listeners with on(), remove them with off(), or register a
single-use listener with once():
const onIncomingMessage = (message) => {
console.log(message.content);
};
Convor.on("message:received", onIncomingMessage);
Convor.off("message:received", onIncomingMessage);
Convor.once("open", (state) => {
console.log("First open", state);
});| Event | Payload |
|---|---|
ready | Complete widget state |
open | Complete widget state |
close | Complete widget state |
show | Complete widget state |
hide | Complete widget state |
unread:change | {count, state} |
message:sent | Message object |
message:received | Message object |
page:change | {url, referrer?} |
visitor:updated | {visitorId} |
reset | State for the new anonymous visitor |
destroy | undefined |
Message events use this shape:
{
id: string;
content: string;
senderType: "visitor" | "operator" | "system";
createdAt: string;
}Use ready() rather than relying on a ready listener when the listener may be
registered after initialization.
Visitor lifecycle
Convor.resetVisitor()
Starts a fresh anonymous visitor and reloads the iframe session:
await Convor.resetVisitor();Use it after the host application logs a user out or switches accounts. The
promise resolves after the new visitor session is ready. logout() is an alias.
Convor.destroy()
Removes the launcher, iframe, styles, listeners, route observers, and active API requests:
Convor.destroy();Call init() again to create a new widget instance.
Method reference
| Method | Returns | Purpose |
|---|---|---|
init(options?) | Promise<WidgetState> | Programmatically create the widget loader |
ready() | Promise<WidgetState> | Wait for visitor authentication and API readiness |
open() / openChat() | void | Open the panel |
close() / closeChat() | void | Close the panel |
toggle() | void | Toggle the panel |
show() | void | Show launcher and panel |
hide() | void | Hide launcher and panel |
isOpen() | boolean | Read open state |
isVisible() | boolean | Read visibility state |
getState() | WidgetState | Read complete synchronous state |
getVisitorId() | string | null | Read current visitor ID |
setPage(page) | Promise<void> | Synchronize a logical route |
track("page_view", properties?) | Promise<void> | Record a page view |
setUser(user) | Promise<void> | Set contact data, metadata, and pre-chat values |
identify(identity) | Promise<void> | Apply server-verified customer identity |
setAttributes(attributes) | Promise<void> | Update visitor application attributes |
setVariables(attributes) | Promise<void> | Alias of setAttributes() |
setMetadata(attributes) | Promise<void> | Alias of setAttributes() |
setDraft(message) | Promise<void> | Prefill the composer |
sendMessage(message) | Promise<Message> | Send a visitor message |
on(event, listener) | void | Add an event listener |
off(event, listener) | void | Remove an event listener |
once(event, listener) | void | Add a one-shot listener |
resetVisitor() / logout() | Promise<void> | Start a new anonymous visitor |
destroy() | void | Remove the widget instance |
Next.js example
The following client component mirrors a common Smartsupp-style integration: it waits for account loading, sends contact fields and labeled variables, and updates the logical page on App Router navigation.
"use client";
import Script from "next/script";
import {usePathname, useSearchParams} from "next/navigation";
import {useCallback, useEffect, useState} from "react";
export function ConvorChat({user, isLoading}) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [scriptReady, setScriptReady] = useState(false);
const syncUser = useCallback(async () => {
if (!window.Convor || isLoading) return;
await window.Convor.ready();
await window.Convor.setUser({
name: user?.name ?? null,
email: user?.email ?? null,
phone: user?.phone ?? null,
attributes: {
customerId: {
label: "Customer ID",
value: user?.id ?? "Guest",
},
currency: {
label: "Currency",
value: user?.currency ?? "",
},
dropShipping: Boolean(user?.isDropShipping),
language: {
label: "Language",
value: user?.language ?? "",
},
vatId: {
label: "VAT ID",
value: user?.vatId ?? "",
},
},
});
}, [isLoading, user]);
useEffect(() => {
if (!scriptReady) return;
void syncUser();
}, [scriptReady, syncUser]);
useEffect(() => {
if (!scriptReady || !window.Convor) return;
const query = searchParams.toString();
void window.Convor.setPage(query ? `${pathname}?${query}` : pathname);
}, [pathname, scriptReady, searchParams]);
return (
<Script
src="https://cdn.convor.io/widget.js"
data-key={process.env.NEXT_PUBLIC_CONVOR_KEY}
strategy="lazyOnload"
onReady={() => setScriptReady(true)}
/>
);
}For authenticated identity, fetch userHash from your own server after
Convor.ready() provides visitorId, then call identify() instead of relying
only on setUser().
Czy ta strona była pomocna?