Skip to content
Convor
Convor Docs
Developer

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.Convor

Convor 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:change on 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,
  },
});
FieldTypePersistence
namestring | nullVisitor profile and matching pre-chat field
emailstring | nullVisitor profile and matching pre-chat field
phonestring | nullVisitor profile and matching pre-chat field
attributesRecord<string, AttributeValue>Visitor metadata shown in the dashboard
departmentstring | nullPre-chat department selection
customFieldsRecord<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;
} | null

Use 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:

  1. The browser obtains the current Convor visitor ID and sends that ID to your authenticated application backend.
  2. 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);
});
EventPayload
readyComplete widget state
openComplete widget state
closeComplete widget state
showComplete widget state
hideComplete widget state
unread:change{count, state}
message:sentMessage object
message:receivedMessage object
page:change{url, referrer?}
visitor:updated{visitorId}
resetState for the new anonymous visitor
destroyundefined

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

MethodReturnsPurpose
init(options?)Promise<WidgetState>Programmatically create the widget loader
ready()Promise<WidgetState>Wait for visitor authentication and API readiness
open() / openChat()voidOpen the panel
close() / closeChat()voidClose the panel
toggle()voidToggle the panel
show()voidShow launcher and panel
hide()voidHide launcher and panel
isOpen()booleanRead open state
isVisible()booleanRead visibility state
getState()WidgetStateRead complete synchronous state
getVisitorId()string | nullRead 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)voidAdd an event listener
off(event, listener)voidRemove an event listener
once(event, listener)voidAdd a one-shot listener
resetVisitor() / logout()Promise<void>Start a new anonymous visitor
destroy()voidRemove 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().

Was this page helpful?

On this page