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.
Add as many web-widget sources as you need at no cost. Everything in this guide uses the free web widget.
Connect WhatsApp numbers to the same Omni channel as paid add-ons, so web and WhatsApp conversations land in one inbox.
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.
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.
Choose Quick embed or JavaScript SDK below, paste it into your page, and replace YOUR_CHANNEL_KEY with the key you copied.
Copera's floating launcher appears in the position you configured. Open it to start a conversation.
- Quick embed
- JavaScript SDK
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.
<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.
Load the same CDN runtime, initialize it with launcher: 'custom' to suppress Copera's floating launcher, and drive the panel from your own control with the CoperaOmni command API.
<button id="support-chat" type="button" aria-controls="copera-support" aria-expanded="false">
Chat with support
</button>
<script>
window.CoperaOmni = window.CoperaOmni || function () {
(window.CoperaOmni.q = window.CoperaOmni.q || []).push(arguments);
};
</script>
<script async src="https://widget.copera.ai/widget.js"></script>
<script>
const launcherButton = document.querySelector('#support-chat');
CoperaOmni('on', 'open', function () {
launcherButton.setAttribute('aria-expanded', 'true');
});
CoperaOmni('on', 'close', function () {
launcherButton.setAttribute('aria-expanded', 'false');
});
CoperaOmni('init', {
channelKey: 'YOUR_CHANNEL_KEY',
launcher: 'custom',
locale: document.documentElement.lang || 'en',
});
launcherButton.addEventListener('click', function () {
CoperaOmni('toggle');
});
</script>
Use open and close when your app already knows the desired state, and toggle for a single launcher button. If you kept the default launcher, setLauncherVisible(false) temporarily hides it and setLauncherVisible(true) restores it.
@copera/omni-widget-sdk is an optional typed wrapper around these same commands. See Typed wrapper and CDN runtime.
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.
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 a channel using the Omni channel type, or open an existing one. A single Omni channel can receive conversations from multiple sources.
Open the channel's Settings, select Add source, then choose Web widget. Copera labels this source as Free.
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.
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.
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.
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.
| Setting | What it controls | Default |
|---|---|---|
| Display name | The name shown at the top of the chat panel. | Source name |
| Primary color | The accent color of the launcher and panel. | Workspace accent |
| Launcher position | Which corner the floating launcher sits in. | Bottom-right |
| Greeting message | An optional first message shown when the panel opens. | None |
| Show a pre-chat form | Asks anonymous visitors for details before the conversation begins. | Off |
| Embed security | How Copera decides which pages may embed the widget. | Browser origin |
| Require verified visitor identity | Blocks 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
| Command | Signature | Purpose |
|---|---|---|
init | CoperaOmni('init', config) | Initialize the widget. config.channelKey is required; locale and launcher are optional. |
identify | CoperaOmni('identify', identity) | Set the visitor identity. Prefer { identityToken } for signed-in users; { name, email } supports unverified visitor details. |
logout | CoperaOmni('logout') | Clear the current identity before a user signs out or another user takes over the page. |
destroy | CoperaOmni('destroy') | Remove the iframe, launcher, timers, listeners, and in-memory credentials. |
open | CoperaOmni('open') | Open the chat panel. |
close | CoperaOmni('close') | Close the chat panel. |
toggle | CoperaOmni('toggle') | Toggle the panel between open and closed. |
setLauncherVisible | CoperaOmni('setLauncherVisible', visible) | Show or hide the built-in launcher without destroying the widget. |
update | CoperaOmni('update', { locale }) | Change the live widget locale. |
on | CoperaOmni('on', eventName, callback) | Subscribe to an event. The live runtime returns an unsubscribe function. |
off | CoperaOmni('off', eventName, callback) | Remove the same callback previously passed to on. |
The supported init options are:
| Option | Type | Default | Description |
|---|---|---|---|
channelKey | string | Required | Public source key copied from Configure & install. |
locale | string | "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.
| Event | Payload | When 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:
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:
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.
- Anonymous
- Pre-chat form
- Verified
Leave Require verified visitor identity off. A visitor can begin without identity data. Copera keeps the browser's widget session associated with the conversation.
Enable Show a pre-chat form to ask the visitor for details before the conversation begins. Information entered by the visitor is useful context, but it is not proof that the visitor owns that identity.
For a signed-in customer, your same-origin backend exchanges its source-bound server API key for a short-lived identity token. The browser passes only that token to identify. Enable Require verified visitor identity when anonymous and pre-chat conversations must be blocked.
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:
- Copera issues a source-bound server API key with the
cp_omni_sk_prefix. - Your backend sends a stable customer identifier to Copera and receives a five-minute identity token.
- Your browser receives only the short-lived token and passes it to the widget.
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.
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.
Your backend calls the identity-token endpoint with the server key:
POST https://api.copera.ai/public/v1/omni-channel/identity-tokens
Authorization: Bearer cp_omni_sk_...
Content-Type: application/json
The request body accepts:
| Field | Required | Description |
|---|---|---|
externalId | Yes | An opaque, stable, non-PII identifier for the signed-in customer, scoped to your application. Use the same value on future visits. |
name | No | Current display name, from your trusted account record. |
email | No | Current 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.
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.
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:
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.
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.
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.com — not https://widget.copera.ai. Configure exact origins, including the scheme and port when present:
| Page URL | Allowed origin |
|---|---|
https://www.example.com/pricing | https://www.example.com |
https://app.example.com/support | https://app.example.com |
http://localhost:3000/account | http://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:
| Action | Immediate effect | Follow-up |
|---|---|---|
| Regenerate key | Existing snippets using the old channelKey stop working. | Replace the key in every embed. |
| Revoke a server API key | Token-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:
CoperaOmni('init', {
channelKey: 'YOUR_CHANNEL_KEY',
locale: 'en',
});
function onApplicationLocaleChanged(locale) {
CoperaOmni('update', { locale });
}
For a single-page application:
- Call
initonce for the mounted widget integration. - Subscribe with
onand remove listeners withoffwhen the owning component unmounts. - Call
logoutwhen the application user signs out or changes. - Call
destroywhen 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:
<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
401from the token endpoint as a missing, malformed, or revoked server key, and rotate or replace it. - Treat
403as 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.aiforwidget.js.frame-src:https://widget.copera.aifor the chat panel iframe.connect-src:https://api.copera.aifor 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'instyle-srconly — never add it toscript-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'inscript-src. - Configure the embedding host page origin under Allowed origins, for example
https://support.example.com— nothttps://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.
Related documentation
How the source-bound backend credential differs from general Public API tokens.
Exact request, response, and error schemas for the identity-token endpoint.
Public API messaging for regular Copera channels.
How the Omni identity-token endpoint fits into the Copera Public API.