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.
<!-- 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.
npm install @zealoop/messenger-js-sdkimport 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)
"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.
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.
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.
| Setting | What it does |
|---|---|
Agent name | The name shown in the widget header. Defaults to Zea. |
Greeting | The opening message, before the visitor types anything. |
Language | Preferred reply language. The gate stage also detects the visitor's language per message. |
Position | Which corner the launcher sits in. |
Allowed origins | Domains 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.
- Include every host you actually serve from, including staging if you test there.
- Subdomains are not implied — list them.
- The origin check is enforced server-side, so removing a domain takes effect immediately, without a cache purge.
Troubleshooting
| Symptom | Usual cause |
|---|---|
| No launcher appears | The publicKey in the snippet does not match a workspace, or the current origin is not in Allowed origins. |
| Answers have no citations | Sources are still crawling or embedding. Only READY chunks are retrievable. |
| Agent says it cannot look that up | The visitor is not identified, or the action requires identity. Check the trace in Inbox. |
identify() silently ignored | The signature did not verify. A wrong secret and a missing secret look identical from the browser — check your server. |