Skip to main content

Copera Omni Web Chat Widget: Setup, SDK, and Identity

The Copera Omni web widget adds web chat to your site and routes every conversation into an Omni channel — a shared inbox that can receive conversations from multiple sources at once, such as one or more web widgets and WhatsApp numbers, so your team works across all of them in one place.

This guide takes you from a two-minute install to a production-grade integration: the copy-paste embed, the CoperaOmni command API for custom launchers, browser-origin security, and the secure flow for identifying signed-in visitors without ever exposing a server key in the browser.

Free vs paid sources

An Omni channel can combine several sources. The web widget is the free one; WhatsApp is a paid add-on.

Web widget — Free

Add as many web-widget sources as you need at no cost. Everything in this guide uses the free web widget.

WhatsApp — Paid add-on

Connect WhatsApp numbers to the same Omni channel as paid add-ons, so web and WhatsApp conversations land in one inbox.

Removing a source does not cancel a purchased number

Removing a WhatsApp source or deleting an Omni channel does not cancel its purchased number slot. To stop paying for a slot, reduce the purchased quantity separately in Workspace Billing.

Quick start

Two minutes to a live widget.

Add a Web widget source

In Copera, open (or create) an Omni channel, go to Settings → Add source, and choose Web widget — Copera labels this source as Free. Then copy the channelKey shown under Configure & install.

Install the snippet before </body>

Choose Quick embed or JavaScript SDK below, paste it into your page, and replace YOUR_CHANNEL_KEY with the key you copied.

Reload your page

Copera's floating launcher appears in the position you configured. Open it to start a conversation.

Place this complete snippet once, immediately before the closing </body> tag. It loads the CDN runtime asynchronously, initializes the source, and shows Copera's floating launcher.

index.html
<script>
(function (w, d) {
w.CoperaOmni = w.CoperaOmni || function () {
(w.CoperaOmni.q = w.CoperaOmni.q || []).push(arguments);
};
var s = d.createElement('script');
s.async = 1;
s.src = 'https://widget.copera.ai/widget.js';
d.head.appendChild(s);
})(window, document);
CoperaOmni('init', { channelKey: 'YOUR_CHANNEL_KEY' });
</script>

That is everything a plain HTML site needs. To control the panel from your own button instead of the floating launcher, switch to the JavaScript SDK tab.

Public key, not a channel ID

The generated channelKey is the public key for this specific web-widget source. It is safe to place in browser code. It is not the Omni channel ID or source ID, and it must not be replaced with either one.

Regenerating the key breaks existing embeds

If you select Regenerate key, the old channelKey stops working. Replace it everywhere the widget is embedded.

Configure the widget

The quick start uses defaults. Open the widget source's Settings (or Configure & install) to tailor the full experience before you go live.

Create or open an Omni channel

Create a channel using the Omni channel type, or open an existing one. A single Omni channel can receive conversations from multiple sources.

Add the web widget source

Open the channel's Settings, select Add source, then choose Web widget. Copera labels this source as Free.

Configure the experience

Enter a Display name, choose the Primary color and Launcher position, and optionally set a Greeting message. You can also enable Show a pre-chat form for anonymous visitors.

Secure the embed

For a new integration, choose Browser origin (recommended) under Embed security. Add every exact site origin that may embed the widget to Allowed origins, pressing Enter after each one — for example, https://www.example.com and https://app.example.com.

Choose the identity behavior

Leave Require verified visitor identity off to allow anonymous or pre-chat conversations. Turn it on when your site will always call identify with a server-issued identity token. The pre-chat form and required verified identity cannot be enabled together.

Create and install

Select Create widget. On the success screen, copy Quick embed for the built-in launcher or select JavaScript SDK for a custom launcher. You can return to the source and select Configure & install later.

SettingWhat it controlsDefault
Display nameThe name shown at the top of the chat panel.Source name
Primary colorThe accent color of the launcher and panel.Workspace accent
Launcher positionWhich corner the floating launcher sits in.Bottom-right
Greeting messageAn optional first message shown when the panel opens.None
Show a pre-chat formAsks anonymous visitors for details before the conversation begins.Off
Embed securityHow Copera decides which pages may embed the widget.Browser origin
Require verified visitor identityBlocks anonymous and pre-chat chats; requires a server-issued identity token.Off

JavaScript command API

https://widget.copera.ai/widget.js installs the global command function CoperaOmni(command, ...args). Calls made through the queue snippet before the runtime finishes loading are buffered and replayed in order.

Commands

CommandSignaturePurpose
initCoperaOmni('init', config)Initialize the widget. config.channelKey is required; locale and launcher are optional.
identifyCoperaOmni('identify', identity)Set the visitor identity. Prefer { identityToken } for signed-in users; { name, email } supports unverified visitor details.
logoutCoperaOmni('logout')Clear the current identity before a user signs out or another user takes over the page.
destroyCoperaOmni('destroy')Remove the iframe, launcher, timers, listeners, and in-memory credentials.
openCoperaOmni('open')Open the chat panel.
closeCoperaOmni('close')Close the chat panel.
toggleCoperaOmni('toggle')Toggle the panel between open and closed.
setLauncherVisibleCoperaOmni('setLauncherVisible', visible)Show or hide the built-in launcher without destroying the widget.
updateCoperaOmni('update', { locale })Change the live widget locale.
onCoperaOmni('on', eventName, callback)Subscribe to an event. The live runtime returns an unsubscribe function.
offCoperaOmni('off', eventName, callback)Remove the same callback previously passed to on.

The supported init options are:

OptionTypeDefaultDescription
channelKeystringRequiredPublic source key copied from Configure & install.
localestring"en"BCP 47 locale for the widget UI, such as "en" or "pt-BR".
launcher"default" | "custom""default"Use "custom" to suppress Copera's launcher and control the panel yourself.

Events

Every event callback receives an object whose name identifies the event.

EventPayloadWhen it fires
ready{ name: 'ready' }The iframe is ready and has dispatched its initial state. This does not prove that backend initialization succeeded.
open{ name: 'open' }The panel opens.
close{ name: 'close' }The panel closes.
identityRequired{ name: 'identityRequired' }A closed conversation is being restarted on a source that requires the visitor to be verified again.
unreadCount{ name: 'unreadCount', count: number }The unread message count changes.
conversationStarted{ name: 'conversationStarted', conversationId: string }A conversation starts and its ID becomes available.

Update a custom badge and record a conversation start:

support-widget.js
const unreadBadge = document.querySelector('#support-unread');

function handleUnread(event) {
unreadBadge.textContent = String(event.count);
unreadBadge.hidden = event.count === 0;
}

function handleConversationStarted(event) {
console.log('Omni conversation started', event.conversationId);
}

CoperaOmni('on', 'unreadCount', handleUnread);
CoperaOmni('on', 'conversationStarted', handleConversationStarted);

// When this page component is removed:
CoperaOmni('off', 'unreadCount', handleUnread);
CoperaOmni('off', 'conversationStarted', handleConversationStarted);

Keep the callback reference if you plan to call off; a new inline function is not the same listener.

Use lifecycle events to keep the host UI synchronized:

support-widget.js
CoperaOmni('on', 'ready', function () {
document.querySelector('#support-chat').disabled = false;
});

CoperaOmni('on', 'open', function () {
document.querySelector('#support-chat').setAttribute('aria-expanded', 'true');
});

CoperaOmni('on', 'close', function () {
document.querySelector('#support-chat').setAttribute('aria-expanded', 'false');
});

CoperaOmni('on', 'identityRequired', function () {
// Ask the signed-in host application to refresh its identity token.
void identifyCurrentUser();
});

Visitor identity

Choose how much you know about each visitor. Anonymous and pre-chat visitors need no backend; verified visitors use a short-lived, server-issued token.

Leave Require verified visitor identity off. A visitor can begin without identity data. Copera keeps the browser's widget session associated with the conversation.

Calling identify with { name, email } supplies unverified visitor details. For authenticated account identity, use { identityToken } instead — described next.

Secure verified identity

Verified identity has three boundaries:

  1. Copera issues a source-bound server API key with the cp_omni_sk_ prefix.
  2. Your backend sends a stable customer identifier to Copera and receives a five-minute identity token.
  3. Your browser receives only the short-lived token and passes it to the widget.
Never expose the server API key

The cp_omni_sk_… key belongs only in your backend secret store. Never place it in HTML, browser JavaScript, a mobile app, logs, analytics properties, or a public repository. The public channelKey and the private server API key serve different purposes and are not interchangeable.

Create a source-bound server key

Open the widget source's Configure & install panel. Under Server API keys, select Create server key, name it for one backend or environment, and copy it immediately — it is shown only once. Multiple active keys are supported, which allows rotation without sharing one credential across environments.

Store it as a backend-only environment variable, for example COPERA_OMNI_SERVER_KEY.

Mint a token from your backend

Your backend calls the identity-token endpoint with the server key:

Request
POST https://api.copera.ai/public/v1/omni-channel/identity-tokens
Authorization: Bearer cp_omni_sk_...
Content-Type: application/json

The request body accepts:

FieldRequiredDescription
externalIdYesAn opaque, stable, non-PII identifier for the signed-in customer, scoped to your application. Use the same value on future visits.
nameNoCurrent display name, from your trusted account record.
emailNoCurrent email address, from your trusted account record.

The server key already binds the request to one workspace and one web-widget source; do not send a workspace ID, source ID, channel ID, or channelKey in this request. See the Create Omni identity token API reference for the exact schema and responses.

The following Node.js route illustrates the same-origin pattern. It assumes your application has already authenticated the request and made the signed-in user available as request.user.

server/routes/omni-identity.js
export async function postOmniIdentity(request, response) {
const user = request.user;
if (!user) {
return response.status(401).json({ message: 'Sign in required' });
}

const coperaResponse = await fetch(
'https://api.copera.ai/public/v1/omni-channel/identity-tokens',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.COPERA_OMNI_SERVER_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
externalId: user.omniExternalId,
name: user.name,
email: user.email,
}),
},
);

if (!coperaResponse.ok) {
return response.status(502).json({ message: 'Unable to start support chat' });
}

const { identityToken, expiresAt } = await coperaResponse.json();
response.setHeader('Cache-Control', 'no-store');
return response.json({ identityToken, expiresAt });
}

For the same stable externalId within that widget source, Copera creates the contact the first time and reuses that same contact on future verified sessions. Use an opaque value created for your application. Do not use a display name, email address, phone number, username, sequential database ID, or another directly identifying or changeable value.

Fetch from the browser, then identify

Expose the route on the same origin as your application, require the application's normal authenticated session and CSRF protections, and return only the short-lived token:

app/support-chat.js
async function identifyCurrentUser() {
const response = await fetch('/api/omni-identity', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.appCsrfToken,
},
});

if (!response.ok) {
throw new Error(`Omni identity request failed: ${response.status}`);
}

const { identityToken } = await response.json();
await CoperaOmni('identify', { identityToken });
}

CoperaOmni('init', { channelKey: 'YOUR_CHANNEL_KEY' });
void identifyCurrentUser().catch(function (error) {
console.error('Unable to identify the Omni visitor', error);
});

Identity tokens expire five minutes after they are minted. Mint them on demand and use them immediately; do not persist them in local storage or treat them as a long-lived login token.

Switching users on the same page

Call and await CoperaOmni('logout') before identifying a different signed-in user in the same browser page. On application sign-out, call logout even if you plan to keep anonymous chat available. This prevents one customer's identity from carrying into another customer's session.

app/support-chat.js
async function switchOmniUser(nextUserIsSignedIn) {
await CoperaOmni('logout');

if (nextUserIsSignedIn) {
await identifyCurrentUser();
}
}

When a visitor restarts a closed conversation and Require verified visitor identity is enabled, the widget emits identityRequired so the host can verify the visitor again. Request a fresh token and call identify; do not fall back to browser-provided name and email as though they were verified. For the initial signed-in visit, identify the visitor proactively after init as shown above.

Browser-origin security

Browser origin (recommended) checks the embedding host page against the source's Allowed origins and establishes a short-lived origin-bound embed session. Add the origin of the page that hosts the widget, such as https://support.example.comnot https://widget.copera.ai. Configure exact origins, including the scheme and port when present:

Page URLAllowed origin
https://www.example.com/pricinghttps://www.example.com
https://app.example.com/supporthttps://app.example.com
http://localhost:3000/accounthttp://localhost:3000

Paths, queries, fragments, usernames, and passwords are not part of an origin. Add each subdomain separately. Prefer HTTPS outside local development.

Choose Browser origin (recommended) for new deployments and configure every permitted origin explicitly. When you add or remove a production domain, update Allowed origins before deploying the embed there. Removing an origin prevents new embed sessions from that origin.

Rotation and revocation

Treat the public embed key and server API keys as separate credentials:

ActionImmediate effectFollow-up
Regenerate keyExisting snippets using the old channelKey stop working.Replace the key in every embed.
Revoke a server API keyToken-minting requests using that cp_omni_sk_… key fail immediately.Deploy a replacement key first if the integration must remain available.

For zero-downtime server-key rotation, create a second key, deploy it to the backend, confirm its Last used value updates, then revoke the old key. Use separate keys for production, staging, and each independent backend.

Locale and lifecycle

Set the initial locale in init, then use update if the visitor changes language:

app/support-chat.js
CoperaOmni('init', {
channelKey: 'YOUR_CHANNEL_KEY',
locale: 'en',
});

function onApplicationLocaleChanged(locale) {
CoperaOmni('update', { locale });
}

For a single-page application:

  • Call init once for the mounted widget integration.
  • Subscribe with on and remove listeners with off when the owning component unmounts.
  • Call logout when the application user signs out or changes.
  • Call destroy when the widget integration itself is permanently removed or when you intentionally need a fresh instance.

Troubleshooting

The widget script does not load

Guard script loading. The direct CDN snippet can detect a network or CSP failure with the script element's error event:

index.html
<script>
(function (w, d) {
w.CoperaOmni = w.CoperaOmni || function () {
(w.CoperaOmni.q = w.CoperaOmni.q || []).push(arguments);
};
var s = d.createElement('script');
s.async = 1;
s.src = 'https://widget.copera.ai/widget.js';
s.onerror = function () {
console.error('The Copera Omni widget could not be loaded.');
};
d.head.appendChild(s);
})(window, document);
CoperaOmni('init', { channelKey: 'YOUR_CHANNEL_KEY' });
</script>

If the request is blocked, check your Content Security Policy and confirm the host page origin is in Allowed origins.

How do I confirm initialization succeeded?

init, identify, logout, and destroy may return promises after the live runtime is installed. Await identity and session transitions and handle rejection. When calling the live dispatcher directly or using the typed wrapper, await init() to confirm backend initialization.

Commands queued before the runtime installs are fire-and-forget; the ready event only confirms that the iframe is ready and its initial state was dispatched — it does not prove backend initialization succeeded.

Identity-token requests fail
  • Return a generic error from your backend; never forward the server key or provider response body to the browser.
  • Retry only transient server failures with bounded backoff.
  • Mint a new token after expiration instead of replaying an old token.
  • Treat 401 from the token endpoint as a missing, malformed, or revoked server key, and rotate or replace it.
  • Treat 403 as an unavailable widget source, and check that the source is connected and not suspended.

Content Security Policy

Allowlist Copera origins for a strict CSP

If your site uses Content Security Policy (CSP), allow the minimum required Copera origins in the relevant directives:

  • script-src: https://widget.copera.ai for widget.js.
  • frame-src: https://widget.copera.ai for the chat panel iframe.
  • connect-src: https://api.copera.ai for requests made by the host page.
  • style-src: The current widget runtime creates style elements and style attributes and does not support a nonce or hash alternative. Strict CSP integration therefore requires 'unsafe-inline' in style-src only — never add it to script-src.
  • Your policy must also allow the inline bootstrap shown in Quick embed. If your policy blocks inline scripts, move the bootstrap into an allowed external file or authorize it with your site's nonce or hash policy. Do not weaken the policy with 'unsafe-inline' in script-src.
  • Configure the embedding host page origin under Allowed origins, for example https://support.example.com — not https://widget.copera.ai. CSP permission does not replace Copera's origin check.

After changing CSP, test under CSP enforcement: verify the launcher, opening the panel, starting a conversation, receiving an unread update, and verified identity in the browser console with no blocked requests.

Typed wrapper and CDN runtime

The CDN runtime at https://widget.copera.ai/widget.js is the widget itself and is all a plain HTML integration needs. @copera/omni-widget-sdk is an optional typed wrapper for TypeScript applications: it injects the same CDN runtime once, buffers early calls, and exposes a typed facade for the same commands and events. It does not replace the CDN widget or change the server identity flow.

Use the direct runtime examples in this guide unless your application already consumes the typed wrapper. Package installation and alternate package-CDN instructions are intentionally outside this embed guide.

Frequently asked questions

Is the Copera Omni web widget free?

The web widget is free. WhatsApp numbers connected to an Omni channel are paid add-ons, and removing a WhatsApp source or deleting its channel does not cancel the purchased number slot. Reduce the purchased quantity separately in Workspace Billing.

Is the widget channelKey safe to use in browser code?

Yes. The channelKey is the public key for one web-widget source and belongs in the embed code. It is not a channel ID, source ID, or private cp_omni_sk_… server API key. Regenerating the channelKey immediately stops embeds that still use the old value.

How do I identify a signed-in visitor securely?

Authenticate the visitor in your application backend, exchange the source-bound cp_omni_sk_… key for a five-minute identity token, and return only that short-lived token to the browser. Then call CoperaOmni('identify', { identityToken }). Never expose the server API key to the browser or a mobile client.

How do I restrict which sites can embed the widget?

Choose Browser origin (recommended) and add every exact permitted origin, including its scheme and port when present, to Allowed origins. Add subdomains separately. Content Security Policy permissions do not replace Copera's allowed-origin check.