Embeddable Chat SDK
The Embeddable Chat SDK lets you put a Conversational AI Employee on your own website as a branded chat widget — a support assistant, a sales concierge, an onboarding helper, or an internal team bot. You drop a single script tag on your page, point it at an AI Employee, and the SDK renders a launcher button and loads the chat experience inside a sandboxed iframe. You control the branding and behavior; Ema handles the conversation.
The SDK ships as a small loader script. The widget that loads inside the iframe is the chat app — the same chat UI Ema uses elsewhere — and the SDK brokers messages between your page and that iframe over postMessage.
The external visitor model
A chatbot on a public marketing page has no logged-in Ema user. The SDK handles this with an external visitor identity:
- On first load, the SDK mints a UUID visitor ID and stores it in the browser's
localStorage, keyed to your embed API key's prefix. This visitor ID — not a logged-in account — is what ties a person's messages together into a conversation across page loads. - A new browser, a private window, or cleared storage simply produces a new anonymous visitor, which is the correct behavior.
- Authentication tokens (JWTs) are never stored in
localStorage— they live in memory only, so an attacker who readslocalStoragecannot exfiltrate a usable token. The visitor ID is the identity; the in-memory JWT is the bearer.
You can attach known information about the visitor through userContext (for example name and email), which is forwarded into every conversation as metadata.
Configure your chatbot in the Channels tab
Set up a web chatbot from the Channels tab of the Integrations page:
- Select New channel and choose the Web channel type.
- Pick the Conversational AI Employee (workflow) the chatbot should run.
- Ema issues an embed API key for the channel. This public, embed-scoped key is what the snippet references — it is shown so you can copy it.
- Add the domains where the chatbot is allowed to load (the allowed-origins list).
- Copy the generated embed snippet and paste it into your site.
Configure once, reflected everywhere. Branding, welcome behavior, and the allowed-origins list are stored server-side per channel. Updating them takes effect on the deployed chatbot without changing the embed snippet.
Restrict where the chatbot loads
The allowed-origins list controls which domains may load the widget for a channel. Add each base URL you deploy on (for example www.example.com). A request from an origin not on the list is rejected, so a copied snippet cannot be used to embed your AI Employee on a site you do not control.
The embed snippet
When the channel UI generates a snippet for you it uses a compact single-script form that loads the SDK and calls EmaChat.init({ apiKey: '...' }) from the script's onload — the SDK publishes window.EmaChat synchronously on load, so the inline call is safe:
<script src="https://chatbot.ema.co/sdk/widget.js" onload="EmaChat.init({ apiKey: 'ck_embed_...' })" async></script>
The SDK must be loaded from its hosted origin (https://chatbot.ema.co/sdk/widget.js, or the equivalent origin for your environment) — it derives the chat host from its own <script> tag, so inlined or eval-loaded bundles are not supported. On the current contract the apiKey is all the snippet needs: the embed key encodes the tenant and AI Employee server-side, and branding is read from the channel's server-side configuration (see Configure your chatbot in the Channels tab).
If you want the SDK to be available before its script finishes loading — for example to call init from your own bootstrap code rather than from onload — buffer calls with the queue pattern analytics SDKs use, then load the script asynchronously:
<script>
(function () {
if (window.EmaChat) return;
window.EmaChat = function (...args) { (window.EmaChat.q = window.EmaChat.q || []).push(args); };
var s = document.createElement('script');
s.async = true;
s.src = 'https://chatbot.ema.co/sdk/widget.js';
document.head.appendChild(s);
})();
EmaChat.init({ apiKey: 'ck_embed_...' });
</script>
The init configuration
EmaChat.init(...) accepts:
| Field | Required | Purpose |
|---|---|---|
apiKey | One of apiKey / jwt | The embed-scoped public key issued for the channel. The SDK exchanges it for a JWT before loading the iframe. |
jwt | One of apiKey / jwt | A caller-minted JWT, used instead of exchanging an apiKey (managed and builder modes). |
aieId | Yes | The AI Employee (workflow) to embed. |
tenantId | Yes | Your tenant ID. |
mode | No | default, managed, or builder. Defaults to default. |
targetElement | No | A CSS selector to mount the widget inside; defaults to a floating launcher button. |
userContext | No | Visitor metadata (e.g. name, email) forwarded into every conversation. |
init requires either an apiKey or a jwt — if neither is present the SDK logs an error and does nothing.
Branding and behavior
Branding and launch behavior are not set through init. The title, logo, colors, fonts, "open by default" toggle, and the "Powered by Ema" badge all live in the channel's server-side configuration, edited in the Channels tab's appearance settings (see Configure your chatbot in the Channels tab). The SDK fetches that configuration after it mints a JWT and applies it to the launcher and the chat app, so a branding change takes effect on every embed without touching the snippet.
The welcome message and welcome buttons, and the feedback options (positive/negative reasons and whether comments are enabled), come from the AI Employee's own configuration rather than the channel, so they stay consistent across every channel where the AI Employee runs.
Legacy snippets. Older embeds passed a configuration object (title, logo, theme, defaultOpen, showPoweredBy) directly to init. That field is no longer part of the documented init contract — it is honored only through the legacy compatibility shim. New embeds configure branding in the Channels tab instead.
Methods
After init, the SDK exposes:
EmaChat.setUserData({ ... })— update the forwarded visitor metadata.EmaChat.sendMessage('text')— send a message from the host page.EmaChat.loadSession('...')— load a prior session.EmaChat.getVisitorId()— resolve a promise with the workflow-scoped visitor ID once the SDK has minted a JWT (default mode); your backend uses it to attest the visitor.EmaChat.refreshAuth()— force a JWT re-mint with the current visitor ID.EmaChat.regenerateVisitorId()— wipe and re-create the visitor ID, then re-mint (used to recover when a different user signs in on the same browser).EmaChat.cleanup()— tear the widget down.
Modes
You choose how the widget authenticates with a mode:
| Mode | What you pass | Use case |
|---|---|---|
default | apiKey | A public chatbot on a marketing site. The SDK exchanges the key for a JWT before loading the iframe. |
managed | jwt | Your backend mints the JWT and the SDK never sees an API key — for embedding the chat for an already-authenticated user. |
builder | jwt | Like managed, but with builder affordances (such as "show work" links) visible. |
Security model
The SDK is built to embed safely on third-party pages:
- The embed
apiKeynever crosses into the iframe — the SDK exchanges it for a JWT and forwards only the JWT. - Outbound messages from the chat app target the trusted parent origin captured at first init (strict origin equality — no substring matching, no wildcard).
- Inbound messages on the SDK side are gated on the iframe's window identity, which is stronger than an origin check.
- The allowed-origins list (configured per channel) governs which domains may load the widget at all.
New integrations should use the EmaChat.init({ apiKey, aieId, ... }) form shown here. A legacy snippet form using projectId / personaId still works through a compatibility shim, but it is deprecated — do not build new embeds against it.
What's next
- Integrations Hub — configure channels and connections.
- Data API — export the conversation data your chatbot produces into your own analytics stack.