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", apiUrl: "https://api.zealoop.com" };
(function () {
var i = function () { i.q.push(arguments); };
i.q = [];
window.Zealoop = i;
var s = document.createElement("script");
s.async = true;
s.src = "https://api.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 and Next.js
Use the hosted loader inside an effect. The npm SDK is not published yet. For Next.js App Router, keep this in a client component and render it once in the root layout. Adapt the window declarations to your TypeScript conventions if needed.
"use client"; // needed for Next.js App Router
import { useEffect } from "react";
export default function ZealoopMessenger() {
useEffect(() => {
window.zealoop = { publicKey: "pk_live_your_key", apiUrl: "https://api.zealoop.com" };
if (!window.Zealoop) {
const z = function () { z.q.push(arguments); };
z.q = []; window.Zealoop = z;
const s = document.createElement("script");
s.async = true; s.src = "https://api.zealoop.com/widget.js";
document.head.appendChild(s);
}
window.Zealoop("boot");
return () => window.Zealoop("shutdown");
}, []);
return null;
}For an authenticated visitor, compute an email signature on your backend and call Zealoop("identify", …) when the user changes. Clear identity on logout. See identity signing.
To have a coding agent perform these steps, connect Zealoop through MCP.
Commands
Zealoop(…) works immediately, even before the bundle loads — calls queue and replay in order.
Zealoop("boot"); // start the widget (call once, after setting window.zealoop)
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.
- Enable origin enforcement in Security after adding your domains. The frame's browser policy checks the actual embedding site on load; changes apply when the messenger reloads. This limits browser embedding and complements request budgets — the public key is not an authentication secret.
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. |