Guides

Install Customer messenger

Messenger runs inside your product for signed-in users identified by a stable ID; email and name are optional. It can also run on public pages for anonymous website visitors. In your workspace, Settings → Customer messenger → Installation shows this guide with your public key already filled in.

Before choosing when Messenger activates, review the privacy and data lifecycle guide for consent, browser storage, announcements, retention, and data-subject rights.

Before you start

  • Signed-in users, and optionally visitors

    In your product, your server signs who each user is. On public pages such as a marketing site, turn on anonymous visitors under Experience and skip the server step.

  • Trusted website origins

    Add every origin that will load Messenger, such as https://app.acme.com and http://localhost:3000.

  • Two server settings

    RIDGETOP_WIDGET_SECRET is the signing secret. RIDGETOP_MESSENGER_ORIGIN is the origin of the page that loads Messenger. Keep both out of browser code.

Step 1: Create a signed identity endpoint

Your server signs the user's stable ID so the browser cannot change it. Email and name are optional profile data. Serve the endpoint from your product's own domain, behind your normal login.

Express route, behind your login middlewarejavascript
import { createHmac } from "node:crypto";

// requireUser: your existing middleware. It must answer 401 (not a redirect)
// when nobody is signed in.
app.get("/api/support-identity", requireUser, (req, res) => {
  const user = req.user;
  const identity = {
    id: String(user.id),
    // The exact origin of the page that loads Messenger.
    origin: process.env.RIDGETOP_MESSENGER_ORIGIN,
    iat: Math.floor(Date.now() / 1000), // seconds, not milliseconds
  };
  // Optional profile data. Omit email to preserve the value already stored;
  // set it to null only when you deliberately want to clear that value.
  // identity.email = user.email;
  // identity.name = user.name;
  if (user.organization) {
    identity.company = {
      id: String(user.organization.id),
      name: user.organization.name,
    };
  }

  const payload = JSON.stringify(identity);

  // Sign the payload string itself, then send that same string.
  const signature = createHmac("sha256", process.env.RIDGETOP_WIDGET_SECRET)
    .update(payload)
    .digest("hex");

  res.set("Cache-Control", "no-store");
  res.json({ payload, signature });
});
  • id is the identity key. A stable id is enough for Messenger, announcements, and product identity. Email and name are optional profile data.
  • Email uses patch semantics. Leave email out to keep the stored value, send a non-empty email to replace it, or send null to deliberately clear it.
  • Sign the exact string you send. Serialize the JSON once, sign it, and return that string unchanged. Re-encoding it on the way out breaks the signature.
  • Use the secret as text. HMAC-SHA256 keyed with RIDGETOP_WIDGET_SECRET exactly as shown, hex-encoded. Do not hex-decode the secret first.
  • Sign the page's origin, not the endpoint's. Set RIDGETOP_MESSENGER_ORIGIN to the origin where Messenger loads, such as https://app.acme.com. It must match a trusted origin exactly.
  • Fresh, in seconds, never cached. iat is Unix time in seconds and expires after 5 minutes. Respond with Cache-Control: no-store, so no proxy or CDN ever serves one user's identity to another.
  • 401 when signed out. Messenger is for signed-in users. Return 401 rather than a redirect, so the browser code can skip Messenger cleanly.
  • company is optional. Send it only for users who belong to an organization, and include both a stable id and a name.

Step 2: Load Messenger in your frontend

Load widget.js from Ridgetop with a script tag, start Messenger once the user is signed in, and shut it down when they sign out so the next person on the device never sees their conversations.

Pages your signed-in users see, before </body>html
<script src="https://app.eu.ridgetop.io/widget.js"></script>
<script>
  async function fetchIdentity() {
    const response = await fetch("/api/support-identity", {
      headers: { Accept: "application/json" },
    });
    if (!response.ok) throw new Error(`Support identity failed: ${response.status}`);
    return response.json(); // { payload, signature }
  }

  // identity is a function so Messenger can renew the session on long visits.
  Ridgetop.boot({
    publicKey: "wpk_your_public_key",
    identity: fetchIdentity,
  });

  // When the user signs out:
  // Ridgetop.shutdown();
</script>

Using another framework? Follow the same pattern: load the script once, call Ridgetop.boot after sign-in with identity as a function, and call Ridgetop.shutdown() on sign-out. Single-page navigation is tracked automatically.

One site with both signed-out and signed-in pages? With anonymous visitors on, have your identity function return null when the endpoint answers 401. Messenger continues as a visitor, and their conversations move to their account when they sign in.

Step 3: Add properties, events, and consentOptional

Enrich conversations, record product events, and pass each consent-manager update to Ridgetop without reloading the page.

Anywhere after widget.js has loadedjavascript
Ridgetop.boot({
  publicKey: "wpk_your_public_key",
  identity: fetchIdentity,

  // Optional properties. Keys must match API keys in Settings → Properties.
  user: { properties: { plan_tier: "growth" } },
  company: { properties: { employee_count: 42 } },

});

// Custom providers: send the current consent-manager state, then call this on
// every change. OneTrust and Cookiebot are read automatically when configured.
// Omitted permissions retain their previous value. false withdraws permission.
Ridgetop.setConsent({
  support: consent.support,
  analytics: consent.analytics,
  replay: consent.replay,
  marketing: consent.marketing,
});

// Inspect provider detection and the resolved category mapping while testing.
Ridgetop.getConsentStatus();

// Product events recorded before analytics activates are discarded.
Ridgetop.track("project.created", { plan: "growth" });

// Open or close Messenger from your own "Contact support" button.
Ridgetop.show();
Ridgetop.hide();

With the custom provider, call Ridgetop.setConsent once with your consent manager's current state and again whenever it changes. OneTrust and Cookiebot are read automatically when selected in the workspace privacy policy. Omitted permissions keep their previous value; send false to withdraw one. Events from before a permission becomes active are discarded, not uploaded later.

Run Ridgetop.getConsentStatus() in the browser console to inspect the selected provider, detected categories, mapped permissions, and missing-provider state. The diagnostic never includes a raw consent string.

Step 4: Check the connection

Open a page with Messenger installed while signed in. The browser console explains any problem with a Ridgetop: message.

Console: "the signed origin … does not match this page's origin"
Set RIDGETOP_MESSENGER_ORIGIN to the exact origin of the page, including scheme and port, with no trailing slash.
Console: "this page's origin is not in Trusted website origins"
Add the origin under Identity & security → Trusted website origins, and save.
Console: "the signature does not match"
Check that the server uses the current signing secret, and returns the same payload string it signed.
Console: "the signed identity is more than 5 minutes old"
Create the identity on every request, with iat in seconds, and make sure the endpoint is not cached.
Console: "load widget.js with a <script> tag"
widget.js was bundled or inlined. Load it from your Ridgetop URL with a script tag instead.
Console: "anonymous visitors are turned off"
A page booted Messenger without an identity. Turn on anonymous visitors under Experience, or pass identity on that page.
No launcher appears
The launcher shows only once a session starts. Check the console for a Ridgetop: message explaining why it did not.
Ridgetop is not defined
boot ran before widget.js finished loading. Call it from the script's onload, or onReady in Next.js.