<zalient-agent>
The Zalient seller assistant, as one tag a shop dashboard drops in. One script, one custom element, a shadow root the host page cannot reach into and that cannot reach back out.
The whole integration
<script src="https://zalient.durbinapp.com/agent.v1.js" async></script> <zalient-agent session-token="eyJhbGciOi..." refresh-url="/api/agent-token" ></zalient-agent>
Everything else lives in the bundle: layout, state, the question blocks, the confirmation cards. Nothing about the widget’s internals is public API, so none of it can break your page and your page cannot break it.
Identity is never an attribute
There is no shop-id, and there will not be one. Your backend mints a short-lived signed token; the widget posts it to ours; ours verifies the signature, issuer, audience, expiry and origin, and answers with the shop. Every shop identity downstream comes from that answer. A seller editing the attribute in devtools gets a failed verification, not another shop’s catalogue.
The session token lives in a private field. Not localStorage, which every script on your page can read.
Three attributes
session-token | required | A short-lived JWT your backend signed. Not an id, not a slug. |
refresh-url | optional | A route on your own origin that mints a fresh one. Without it the widget stops when the first token expires. |
theme | optional | light · dark · auto. Absent behaves as auto. |
Three events
zalient:ready | { shop } | The session was established. |
zalient:action | { action, kind, summary } | A write landed. Your cue to refetch. |
zalient:error | { message, fatal } | For your telemetry. The widget shows its own errors. |
document.addEventListener("zalient:action", (event) => {
// { action, kind, summary } — a write landed. Refetch your table.
ordersTable.refetch();
});All three set composed: true, so they cross the shadow boundary. Outside it, event.target is <zalient-agent> rather than whatever was clicked inside.
Theming
Six custom properties are public, and each is a promise kept across versions: --zl-accent, --zl-surface, --zl-ink, --zl-radius, --zl-font, --zl-height. No ::part(): parts are a commitment to an internal structure, and the internal structure is what we most want to stay free to change.
What you need to allowlist
script-src https://zalient.durbinapp.com connect-src https://agent.zalient.shop
That is the whole list. No eval, no inline styles injected into your document, no external font or image. The widget touches no global except the element name and one frozen object with two strings for support triage.