> Source: https://builder.ema.ai/v2/integrations-data/embeddable-chat-sdk
> Title: Embeddable Chat SDK

# 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 reads `localStorage` cannot 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:

1.  Select **New channel** and choose the **Web** channel type.
2.  Pick the Conversational AI Employee (workflow) the chatbot should run.
3.  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.
4.  Add the domains where the chatbot is allowed to load (the allowed-origins list).
5.  Copy the generated embed snippet and paste it into your site.

> [INFO]
> **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:

```html
<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](#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:

```html
<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](#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.

> [INFO]
> **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 `apiKey` **never** 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.

> [TIP]
> 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](/builder/v2/integrations-data/integrations-hub) — configure channels and connections.
-   [Data API](/builder/v2/integrations-data/data-api) — export the conversation data your chatbot produces into your own analytics stack.
