WhatsApp API Platform

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.

Surfacerender nameRequiredPage
Chatbot builder"chatbot"chatbotIdChatbot
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.

OptionTypeRequiredDescription
targetstring | ElementyesCSS selector or element to mount into.
apiKeystringone of theseAPI key (pm_…).
authTokenstringone of theseJWT. Wins over apiKey when both are set.
theme"light" | "dark" | "system"noColour theme.
baseUrlstringnoOverride the app origin (white-label / non-prod). Defaults to https://new.theultimate.io.
onobjectnoMap 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" handlers

Handlers 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

How is this guide?

On this page