Overview
Embed the chatbot builder, template creator, or team chat directly in your own app with a single script and one render call.
The Embed SDK drops a PingMate surface — the chatbot builder, the template creator, or team chat — straight into a page on your own site. It mounts the surface as an iframe, handles auth, and relays events back to your page.
One script, one global (Embed), one call shape for every surface. Adding more surfaces later changes nothing about how you integrate.
Load the script
Serve the loader once per page. Use defer so it never blocks rendering and runs after the DOM is parsed.
<script src="https://new.theultimate.io/embed.js" defer></script>For the fastest first paint, preload the loader and warm the connection to the app origin:
<link rel="preconnect" href="https://new.theultimate.io" crossorigin />
<link rel="preload" as="script" href="https://new.theultimate.io/embed.js" />
<script src="https://new.theultimate.io/embed.js" defer></script>The loader defines a single global, Embed.
Mount a surface
Give the surface a container with a real height, then call Embed.render(product, options).
<div id="embed" style="height: 720px"></div>
<script>
var instance = Embed.render("chatbot", {
target: "#embed", // CSS selector or Element (required)
chatbotId: "cb_123", // surface-specific (required for "chatbot")
authToken: "<jwt>", // or apiKey: "pm_xxx"
theme: "system", // "light" | "dark" | "system"
on: {
ready: function (m) {},
saved: function (m) {},
published: function (m) {},
},
});
</script>render returns an instance handle — use it to subscribe to more events or tear the embed down. It returns null (and logs why) if a required option is missing or the target isn't found.
Declarative alternative
Prefer markup over JS? Every surface can also mount with a data-embed
attribute and zero render code — see Declarative mounting.
Pick a surface
The first argument to render is the surface name. Each page documents its required options and events.
| Surface | render name | Required | Page |
|---|---|---|---|
| Chatbot builder | "chatbot" | chatbotId | Chatbot |
| Template creator | "template" | — | Template |
| Broadcast creator | "broadcast" | — | Broadcast |
| Broadcast list | "broadcastList" | — | Broadcast List |
| Team chat | "teamChat" | — | Team Chat |
Common options
These apply to every surface. Surface-specific options are on each surface's page.
| Option | Type | Required | Description |
|---|---|---|---|
target | string | Element | yes | CSS selector or element to mount into. |
apiKey | string | one of these | API key (pm_…). |
authToken | string | one of these | JWT. Wins over apiKey when both are set. |
theme | "light" | "dark" | "system" | no | Colour theme. |
baseUrl | string | no | Override the app origin (white-label / non-prod). Defaults to https://new.theultimate.io. |
on | object | no | Map of event name → handler. See Events. |
Authentication
Pass one of apiKey or authToken. When both are present, authToken takes precedence.
Don't ship a raw API key to the browser
An apiKey (pm_…) is a long-lived secret that can spend wallet balance.
Anything in a page's HTML is public. For browser embeds, mint a short-lived,
scoped authToken (JWT) on your server and pass that. Reserve apiKey
for trusted server-rendered or internal contexts.
The loader passes your page's origin to the iframe as parentOrigin, and the surface only ever posts events back to that exact origin — never to * — so event payloads can't leak to another frame.
Events
Every surface emits a ready event plus its own lifecycle events. You can handle them two ways, and they can be combined:
Declare handlers up front in the on option:
Embed.render("chatbot", {
target: "#embed",
chatbotId: "cb_123",
authToken: "<jwt>",
on: {
ready: function (m) {},
published: function (m) {},
},
});Subscribe after rendering — handy for wiring events conditionally:
var instance = Embed.render("chatbot", {
target: "#embed",
chatbotId: "cb_123",
authToken: "<jwt>",
});
instance.on("published", function (m) {});
instance.once("ready", function (m) {});
instance.off("published"); // remove all "published" handlersHandlers receive the event payload object. See each surface page for the events it fires.
Sizing
The iframe is height: 100% of its container, so the container needs a height. If it has none, the loader falls back to a sensible default per surface (720px for the builder/creator, 600px for team chat). Setting an explicit height is best, especially inside flex or grid layouts.
Single-page apps
The loader attaches a global message listener per instance. In a SPA, always destroy() the instance when the view unmounts so listeners and the iframe are cleaned up.
import { useEffect, useRef } from "react";
function ChatbotEmbed({ chatbotId, authToken }) {
const ref = useRef(null);
useEffect(() => {
const instance = window.Embed.render("chatbot", {
target: ref.current, // pass the Element directly
chatbotId,
authToken,
theme: "system",
});
return () => instance && instance.destroy();
}, [chatbotId, authToken]);
return <div ref={ref} style={{ height: 720 }} />;
}Make sure embed.js is loaded before your component runs (a <script> in your HTML shell, or load it dynamically and await window.Embed).
Next
- Chatbot embed — mount the flow builder for one chatbot.
- Template embed — mount the template creation wizard.
- Broadcast embed — mount the broadcast creation wizard.
- Broadcast list embed — mount the broadcast list and reports, with optional in-frame create.
- Team chat embed — mount the team-chat workspace.
- API reference — every method, option, event, and the declarative
data-embedform.
How is this guide?


