Install the widget
Embed code
Each site has its own key. Copy the embed code from Sites → your site and paste it before </body>:
<script src="https://cdn.sealreply.com/widget.js" data-site-key="sk_…" async></script>- The widget loads only on the site's allowed domains (and their subdomains), after the first domain is verified. On other domains it stays invisible.
- It renders inside a Shadow DOM, so your page's CSS does not change it and its CSS does not leak into your page.
- It follows the visitor's browser language unless the site fixes one, and remembers returning visitors in
localStorage.
Content Security Policy
If your site sends a CSP header, allow:
script-src https://cdn.sealreply.com;
connect-src https://api.sealreply.com wss://rt.sealreply.com;
img-src https://api.sealreply.com;JavaScript API
When the script has loaded, window.SealReply controls the widget:
| Call | What it does |
|---|---|
SealReply.open() | Opens the chat window. |
SealReply.close() | Closes it. |
SealReply.on(event, handler) | Calls handler on an event (below). |
SealReply.off(event, handler) | Stops calling it. |
SealReply.identify(identity) | Tells the widget who your signed-in user is (see Identify signed-in users). |
SealReply.reset() | Forgets the visitor, e.g. when they sign out. The next chat starts afresh. |
SealReply.version | The widget's version. |
| Event | When | handler receives |
|---|---|---|
ready | The widget has loaded | nothing |
open / close | The chat window opens or closes, by the visitor or by your code | nothing |
message | An agent's (or your integration's) message arrives | { id, text, content_type, created_at } |
The script loads async, so wait for it before calling SealReply. Either listen for the sealreply:ready event on window, or check whether it has already loaded:
<button id="help">Chat with us</button>
<script>
function whenReady(fn) {
if (window.SealReply) fn();
else window.addEventListener("sealreply:ready", fn, { once: true });
}
whenReady(() => {
document.getElementById("help").onclick = () => SealReply.open();
SealReply.on("message", (m) => console.log("New reply:", m.text));
});
</script>Errors thrown by your handlers are caught, so they cannot break the chat.
Identify signed-in users
If your site has its own sign-in, the widget can tell your agents who they are talking to: the visitor appears under their own name, as Verified, and is the same visitor on every device. To stop anyone from posing as someone else, each user's ID is signed on your server with a secret only you and SealReply know.
In Sites → your site → Visitor identity verification, choose Turn on and copy the secret. It is shown once; keep it on your server like a password.
On your server, compute the user's
identity_hash: hex HMAC-SHA256 of their ID, keyed with the secret.jsimport { createHmac } from "node:crypto"; const identityHash = createHmac("sha256", process.env.SEALREPLY_IDENTITY_SECRET) .update(String(user.id)) .digest("hex");php$identityHash = hash_hmac('sha256', (string) $user->id, getenv('SEALREPLY_IDENTITY_SECRET'));pythonimport hmac, hashlib, os identity_hash = hmac.new(os.environ["SEALREPLY_IDENTITY_SECRET"].encode(), str(user.id).encode(), hashlib.sha256).hexdigest()gomac := hmac.New(sha256.New, []byte(os.Getenv("SEALREPLY_IDENTITY_SECRET"))) mac.Write([]byte(userID)) identityHash := hex.EncodeToString(mac.Sum(nil))On pages where the user is signed in, pass it to the widget before
widget.js:html<script> window.sealreplySettings = { identity: { external_id: "42", // the user's ID on your site (up to 128 characters) identity_hash: "<from your server>", name: "Ana López", // optional, shown to agents email: "ana@example.com", // optional }, }; </script> <script src="https://cdn.sealreply.com/widget.js" data-site-key="sk_…" async></script>In a single-page app, call
SealReply.identify({ external_id, identity_hash, name, email })once the user signs in (the widget reloads as that user), andSealReply.reset()when they sign out.
What happens:
- With a valid signature, the visitor is bound to
external_id: their conversations follow them across devices.external_idis checked exactly as you signed it (no trimming or case changes). - Only
external_idis signed.nameandemailfill in the visitor's details when none are noted yet, and never replace what your agents entered; treat them as what the page said, not as verified. - A missing or wrong signature, or a site where verification is off, simply leaves the visitor anonymous; the widget keeps working.
- After a user signs out, the browser's anonymous ID no longer opens their conversations; only a valid signature does. Still call
SealReply.reset()on sign-out: otherwise an anonymous chat started afterwards on a shared computer would be attached to the next user who signs in there. - Signed-in visitors can open their earlier chats on your site from the widget's menu, on any device. Only chats begun since they were first signed in are listed, so whatever someone did anonymously on a shared computer before stays out. Treat
identity_hashlike a password: anyone who has it can read that user's chat history, so sign it on your server and never put it in a URL or a page other people can see. - A signature does not expire. If one may have leaked, generate a new secret.
- Generate new secret retires the old one at once, so update your server first. Turn off makes every visitor anonymous again.
WARNING
Compute identity_hash on your server only. Never put the secret in your pages or apps.
Pass member details
You can also show agents details such as a member's plan, number of orders or total spend: they appear in the Visitor tab beside the conversation, so nobody needs to look them up. So that a visitor cannot edit them in the browser (say, to "Diamond"), these are signed on your server too.
Prepare the attributes: keys up to 40 characters, without
=or line breaks; values up to 200 characters, without line breaks; at most 30.Build the text to sign: the user ID, then one
key=valueline per attribute sorted by key, all joined with a newline\n. For user42and{ plan: "Gold", orders: "12" }:text42 orders=12 plan=GoldSign that text with HMAC-SHA256 and the same secret to get
attributes_hash:jsconst attributes = { plan: "Gold", orders: "12" }; const message = [String(user.id), ...Object.keys(attributes).sort().map((k) => `${k}=${attributes[k]}`)].join("\n"); const attributesHash = createHmac("sha256", process.env.SEALREPLY_IDENTITY_SECRET).update(message).digest("hex");php$attributes = ['plan' => 'Gold', 'orders' => '12']; ksort($attributes, SORT_STRING); $lines = [(string) $user->id]; foreach ($attributes as $k => $v) { $lines[] = "$k=$v"; } $attributesHash = hash_hmac('sha256', implode("\n", $lines), getenv('SEALREPLY_IDENTITY_SECRET'));pythonattributes = {"plan": "Gold", "orders": "12"} message = "\n".join([str(user.id)] + [f"{k}={attributes[k]}" for k in sorted(attributes)]) attributes_hash = hmac.new(os.environ["SEALREPLY_IDENTITY_SECRET"].encode(), message.encode(), hashlib.sha256).hexdigest()gokeys := make([]string, 0, len(attributes)) for k := range attributes { keys = append(keys, k) } sort.Strings(keys) lines := []string{userID} for _, k := range keys { lines = append(lines, k+"="+attributes[k]) } mac := hmac.New(sha256.New, []byte(os.Getenv("SEALREPLY_IDENTITY_SECRET"))) mac.Write([]byte(strings.Join(lines, "\n"))) attributesHash := hex.EncodeToString(mac.Sum(nil))Pass them with the identity:
jswindow.sealreplySettings = { identity: { external_id: "42", identity_hash: "<signature of the user ID>", attributes: { plan: "Gold", orders: "12" }, attributes_hash: "<signature of the attributes>", }, };
With a valid signature the attributes replace the visitor's member details as a whole; with a wrong or missing one they are ignored and identification works as usual. Use only ASCII letters, digits and underscores in keys (such as total_spent), so every language's default sort gives the same order; values can be any text.