Installing the widget

One script tag, loaded asynchronously. Everything else is configuration you set in the dashboard, not attributes you maintain in markup.

The snippet

Paste this before </body>. It is a loader, not the widget — a ~1 KB stub that queues any early Zealoop(…) calls, then fetches the real bundle asynchronously. It can never block or break your page, and it never changes: widget updates ship on our side.

html
<!-- Zealoop messenger -->
<script>
  window.zealoop = { publicKey: "pk_live_your_key" };
  (function () {
    var i = function () { i.q.push(arguments); };
    i.q = [];
    window.Zealoop = i;
    var s = document.createElement("script");
    s.async = true;
    s.src = "https://cdn.zealoop.com/widget.js";
    document.head.appendChild(s);
  })();
</script>

Your public key is on the dashboard home, and it is not a secret — it identifies which workspace the widget belongs to. The widget secret is the one that must never appear in a page.

React / npm

For React apps, install the SDK instead of pasting the snippet. It does the same thing — sets up the queue, injects the loader once — and is safe to call during render and on the server.

shell
npm install @zealoop/messenger-js-sdk
jsx
import Zealoop from "@zealoop/messenger-js-sdk";

export default function App() {
  Zealoop({
    publicKey: "pk_live_your_key",
    userId: user.id,      // your signed-in user's id
    name: user.name,
    email: user.email,
    signature: user.zealoopSignature, // HMAC from your backend — verified identity
  });

  return <YourApp />;
}

Next.js (App Router)

components/zealoop-messenger.tsx
"use client";

import Zealoop from "@zealoop/messenger-js-sdk";

export function ZealoopMessenger({ user }) {
  Zealoop({
    publicKey: "pk_live_your_key",
    ...(user && {
      userId: user.id,
      name: user.name,
      email: user.email,
      signature: user.zealoopSignature,
    }),
  });

  return null; // render once in app/layout.tsx
}

Commands

Zealoop(…) works immediately, even before the bundle loads — calls queue and replay in order.

javascript
Zealoop("open");     // open the messenger
Zealoop("close");    // close it
Zealoop("toggle");
Zealoop("shutdown"); // remove the widget entirely (e.g. on logout)

Identifying the visitor

Tell the widget who is signed in. Without this the agent treats every visitor as anonymous and refuses anything gated on identity.

javascript
Zealoop("identify", {
  email: "customer@example.com",
  name: "Ava Chen",
  signature: "<hmac-sha256 of the email, computed on your server>"
});

Configuration

Appearance and behaviour live in the dashboard under Configuration, so changing them never means a deploy.

SettingWhat it does
Agent nameThe name shown in the widget header. Defaults to Zea.
GreetingThe opening message, before the visitor types anything.
LanguagePreferred reply language. The gate stage also detects the visitor's language per message.
PositionWhich corner the launcher sits in.
Allowed originsDomains permitted to load this workspace's widget. Empty means any origin.

Allowed origins

Add your production domains once you are live. With the list empty, any site that knows your workspace id can mount the widget and consume your conversation quota. It is a short list and it is worth setting.

Troubleshooting

SymptomUsual cause
No launcher appearsThe publicKey in the snippet does not match a workspace, or the current origin is not in Allowed origins.
Answers have no citationsSources are still crawling or embedding. Only READY chunks are retrievable.
Agent says it cannot look that upThe visitor is not identified, or the action requires identity. Check the trace in Inbox.
identify() silently ignoredThe signature did not verify. A wrong secret and a missing secret look identical from the browser — check your server.