Skip to content

Install the widget ​

Embed code ​

Each site has its own key. Copy the embed code from Sites → your site and paste it before </body>:

html
<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:

CallWhat 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.versionThe widget's version.
EventWhenhandler receives
readyThe widget has loadednothing
open / closeThe chat window opens or closes, by the visitor or by your codenothing
messageAn 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:

html
<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.

  1. 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.

  2. On your server, compute the user's identity_hash: hex HMAC-SHA256 of their ID, keyed with the secret.

    js
    import { 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'));
    python
    import hmac, hashlib, os
    
    identity_hash = hmac.new(os.environ["SEALREPLY_IDENTITY_SECRET"].encode(), str(user.id).encode(), hashlib.sha256).hexdigest()
    go
    mac := hmac.New(sha256.New, []byte(os.Getenv("SEALREPLY_IDENTITY_SECRET")))
    mac.Write([]byte(userID))
    identityHash := hex.EncodeToString(mac.Sum(nil))
  3. 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), and SealReply.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_id is checked exactly as you signed it (no trimming or case changes).
  • Only external_id is signed. name and email fill 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_hash like 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.

  1. Prepare the attributes: keys up to 40 characters, without = or line breaks; values up to 200 characters, without line breaks; at most 30.

  2. Build the text to sign: the user ID, then one key=value line per attribute sorted by key, all joined with a newline \n. For user 42 and { plan: "Gold", orders: "12" }:

    text
    42
    orders=12
    plan=Gold
  3. Sign that text with HMAC-SHA256 and the same secret to get attributes_hash:

    js
    const 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'));
    python
    attributes = {"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()
    go
    keys := 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))
  4. Pass them with the identity:

    js
    window.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.