Embed SDK
Ester runs in a pop-out window that you host and open from your application. This guide covers the snippet you add to that page, how to open it, and the two calls that start a session.
Everything below targets production: https://ester-embed.wealth.com.
Add Ester to your pop-out page
If you already host a pop-out page, this is everything you need to add to it — a container, two script tags, and three calls:
<!-- 1. Where the chat goes. Needs a real height — see below. -->
<div id="ester-root"></div>
<!-- 2. Holds your calls until the SDK loads. Must come before the SDK tag. -->
<script>
/* Ester command-queue stub — buffers calls until the SDK loads. SNIPPET_VERSION 1.0.0 */
window.Ester = window.Ester || function () {
(window.Ester.q = window.Ester.q || []).push(arguments);
};
</script>
<!-- 3. The SDK. -->
<script async src="https://ester-embed.wealth.com/v1/sdk/ester-sdk.js"></script>
<!-- 4. Start the session. -->
<script>
Ester('init', { container: 'ester-root' });
// Your backend mints the token for the signed-in advisor.
fetch('/your-backend/ester-token')
.then(function (r) { return r.json(); })
.then(function (data) { Ester('identify', { token: data.session_token }); });
// The chat asks for more width when it needs it (e.g. a flowchart).
Ester('on', 'resizerequest', function (p) { window.resizeBy(p.widthDelta, 0); });
</script>
That first script tag has to come before the SDK tag. It holds onto anything you call, so init() and identify() are safe to call before the SDK has finished downloading.
Please don't add an integrity (SRI) attribute to the script tag. There is one /v1/ path per major version, not one per build — we ship small fixes to it in place, so a pinned hash would start rejecting the script. Add the exact URL to your CSP instead, as described in Origins and CSP. If you need a checksum for an audit trail, the current one is published at https://ester-embed.wealth.com/v1/manifest.json.
container takes an element id, not a CSS selector. The SDK fills that element completely, so resizing the window resizes the chat — you don't need any resize code of your own. The element needs a real height, and so does every parent above it, all the way up to <html>:
html, body, #ester-root { height: 100%; margin: 0; }
#ester-root { width: 100%; }
On a page that already exists, this is the most likely thing to trip you up. If any parent is height: auto, the chat has no height and the window just looks empty — with no error to tell you why. A flex or grid parent that gives the element a real height works just as well.
Complete page, if you're starting from scratch
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ester</title>
<style>
html, body, #ester-root { height: 100%; margin: 0; }
#ester-root { width: 100%; }
</style>
</head>
<body>
<div id="ester-root"></div>
<script>
/* Ester command-queue stub — buffers calls until the SDK loads. SNIPPET_VERSION 1.0.0 */
window.Ester = window.Ester || function () {
(window.Ester.q = window.Ester.q || []).push(arguments);
};
</script>
<script async src="https://ester-embed.wealth.com/v1/sdk/ester-sdk.js"></script>
<script>
Ester('init', { container: 'ester-root' });
// Your backend mints the token for the signed-in advisor.
fetch('/your-backend/ester-token')
.then(function (r) { return r.json(); })
.then(function (data) { Ester('identify', { token: data.session_token }); });
// The chat asks for more width when it needs it (e.g. a flowchart).
Ester('on', 'resizerequest', function (p) { window.resizeBy(p.widthDelta, 0); });
</script>
</body>
</html>
Open it from your application
Open your page in a pop-out. These are the dimensions and window features we use in our own reference implementation, docked to the bottom-right of the opening window:
var width = 500;
var height = 730;
// Don't clamp these to 0: screenX and screenY are negative on a monitor
// placed left of or above the primary one, and clamping would drag the
// pop-out onto the primary monitor instead of docking it to this window.
var left = window.screenX + window.outerWidth - width;
var top = window.screenY + window.outerHeight - height;
var win = window.open(
'/your-ester-popout-page',
'ester-popout',
'popup=yes,width=' + width + ',height=' + height +
',menubar=no,toolbar=no,location=no,status=no,left=' + left + ',top=' + top
);
if (!win) { /* popup blocked — prompt the advisor to allow popups */ }
Reusing the same window name (ester-popout) means a second click focuses the existing window instead of opening another.
The pop-out window handles its own resize, maximize and close. Ester draws no title bar or buttons of its own in this mode.
Getting the token to the pop-out. Simplest is to have the pop-out page fetch it from your backend itself, as in the snippet above. If the opener already holds a token, sessionStorage is cloned into the pop-out at window.open() time and works well. Either way, never put the token in a query string.
Mint a session token
Your backend asks us for the token, one per advisor session, by calling POST /v1/ester/auth on sentinel-api.wealth.com over mutual TLS — meaning both sides present a certificate. The client certificate you received at enrollment is what identifies you; there is no Authorization header and no API key. The full request and response are on the Authentication page.
Give each advisor an id that never changes. The first time we see an advisor.external_id we create a Wealth advisor record for it, and we reuse that same record every time the id comes back. If the id changes, that advisor gets a brand new, empty Ester with none of their history. Pick a stable id from your own system and keep it. Do the same for firm.external_id.
Use a client that speaks HTTP/2. The endpoint sits behind a load balancer set up for HTTP/2, and a client that only speaks HTTP/1.1 is turned away with a 464 before it reaches us. curl --http2 and most standard HTTP libraries are fine; a bare Node https request is the one that usually trips.
Two things about the token itself:
- It lasts 15 minutes and belongs to one advisor at one firm, so don't share it between advisors or hold onto it past that.
- It lives in the browser. Keep it out of URLs, logs and analytics —
identify()is the only way it should reach the SDK.
To check your certificate works before you wire anything up, GET /healthz on the same host over the same connection returns {"status":"ok"} without creating a session.
Keep the session alive
When a token runs out, the SDK fires session_expired. It does not refresh on its own — get a new token and call identify() again. You have about 15 seconds before the message that was waiting fails.
Ester('on', 'session_expired', async () => {
const { session_token } = await fetch('/your-backend/ester-token').then((r) => r.json());
Ester('identify', { token: session_token });
});
Calling identify() again swaps the token in place. It does not reset the conversation.
Origins and CSP
Send us the web address of your pop-out page — the https://, the hostname, and the port if it isn't the default. Ester will not load from an address we haven't added to our list, and it fails quietly: no error, just an empty window. If the pop-out is served from a different address than the page that opens it, it's the pop-out's address we need. Adding one means a rebuild and redeploy on our side, so please give us a few days' notice rather than asking on the day.
Your pop-out page's CSP needs both of these, or the chat will not load:
script-src https://ester-embed.wealth.com
frame-src https://ester-embed.wealth.com
Reference
Configuration — init(config). In pop-out mode these are the options that apply:
| Option | Default | Notes |
|---|---|---|
container | — | Element id the chat fills. This is what selects pop-out mode. |
title | 'Ester' | The title screen readers read out. |
iframeUrl | built in | Override only if we ask you to. |
Events — subscribe with Ester('on', name, handler):
| Event | Payload |
|---|---|
ready | — the chat has loaded and is connected |
session_expired | — get a new token and call identify(), see Keep the session alive |
resizerequest | { widthDelta: number } — how much wider to make the window; a negative number means shrink back |
notification | { title?, body?, level?: 'info' | 'warning' | 'error' } |
error | Error |
destroy | — the SDK was shut down, via Ester.destroy() |
The other events (open, close, statechange, unread) are for the floating launcher and window buttons, which pop-out mode does not use.
init() never throws an error into your page — bad configuration comes through the error event instead, and a second init() call is ignored. identify() has to come after init(); on() can be called at any time.
Versioning. The URL carries only the major version. We ship small fixes to /v1/ in place; a new path (/v2/) appears only if we have to make a breaking change, and we will tell you first. The current protocol is ester-embed/1.0.
Checking it works
This tells you the SDK is installed and starting up correctly:
- Open the pop-out. The script loads and the chat appears straight away — there is no launcher to click.
Ester('on', 'ready', …)fires once the chat is connected. That tells you your address is on our list and your CSP is right; it happens before the token is involved.- Resize the window; the chat should follow with no gaps.
Ester('on', 'error', …)stays quiet.
If ready never fires, it is usually an address we haven't added to our list, or a missing frame-src.