SDK

Web SDK

There is nothing to install for the web: one <iframe> and a short script that speaks the bridge over postMessage. The host page below is the complete integration; the same page with a bridge log is part of the Octolit Embed SDK package.

#Install

  • Your page's origin (scheme + host) is on the brand's frame allow-list - Octolit sets the module's CSP frame-ancestors per brand. Without it the browser shows an empty frame.
  • Your page is served over https; the module's origin is https://live.your-brand.com.
  • Your backend can mint one-time tokens (SSO guide, scope sso:issue).
index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="robots" content="noindex, nofollow" />
  <title>Your app - trading</title>
  <style>
    body { margin: 0; }
    main { height: 100vh; padding: 12px; box-sizing: border-box; }
    iframe { width: 100%; height: 100%; border: 0; border-radius: 12px; }
  </style>
</head>
<body>
  <main>
    <!-- allow="camera; microphone": the KYC page can take ID photos inside the frame -->
    <iframe id="mod" title="Trading" allow="camera; microphone"></iframe>
  </main>
  <script src="/host.js"></script>
</body>
</html>

#Configure: the module URL

Options go on the module URL as query parameters. The module validates every value: a bad colour, radius or font URL is ignored, an unknown screen falls back to the default.

host.js - load
// the trader address of your brand - events are accepted only from here, commands sent only to here
var MODULE_ORIGIN = 'https://live.your-brand.com';
var frame = document.getElementById('mod');

function moduleUrl(extra) {
  var p = new URLSearchParams({
    embed: '1',                          // embed mode on
    transport: 'iframe',
    origin: window.location.origin,      // your page's origin: required to receive the session in the login event
    screen: 'terminal',
    theme: 'dark', primary: '#ff7a1a', bg: '#101418', radius: '14',
    deposit: 'host',                     // Deposit button -> deposit_requested
    links: 'host',                       // outside links -> open_external
    lock: '1', lock_after: '300'         // lock screen after 5 min in the background
  });
  Object.keys(extra || {}).forEach(function (k) { p.set(k, extra[k]); });
  return MODULE_ORIGIN + '/?' + p.toString();
}

frame.src = moduleUrl();
OptionDefaultMeaning
embed1Turns embed mode on. Always send it.
transportiframeWho is hosting; decides the event channel (window.parent.postMessage) and links=auto.
origin-Your page's origin. Required to receive the session in the login event.
screenterminalFirst screen: terminal, wallet, history, account, positions.
account-Trading account id to select.
langbrowser languageUI language: en, de, es, pt, ar, …
theme-light or dark.
primary / bg / paper / text-#RRGGBB accent, page background, card background (default = bg), main text (4.5:1 check).
radius-Corner radius in px, 0 - 32.
font / font_url-Font family name and an https stylesheet URL that loads it.
hide / showheader,nav,brandingComma lists of the module's chrome to hide, and to take out of hide again.
depositmodulehost = the Deposit button only sends deposit_requested.
linksauto (= module on iframe)host = outside links come to you as open_external; module = opened inside the frame.
lock / lock_after0 / 300Lock screen on; seconds in the background before it locks (10 - 86 400).
pro-0 = simple terminal only.
login_form-1 = keep the module's own sign-in form when there is no session.
token-A one-time login token on the first load (or use the login command).

The full option list with the native-only keys (session, injected options) is in the embed guide.

#The bridge

Commands are objects with source: "qe-host" posted to the frame's window, targeted at the module's origin. Events arrive as message events on your window with source: "qe-embed". Check the origin and the source on every message.

host.js - bridge
// host -> module
function send(cmd) {
  if (!frame.contentWindow) return;
  frame.contentWindow.postMessage(Object.assign({ source: 'qe-host' }, cmd), MODULE_ORIGIN);
}

// module -> host
window.addEventListener('message', function (ev) {
  if (ev.origin !== MODULE_ORIGIN) return;             // 1. only the module's origin
  var m = ev.data;
  if (!m || m.source !== 'qe-embed') return;           // 2. only bridge events
  var data = m.data || {};
  switch (m.type) {
    case 'ready':             hideSplash(); break;
    case 'login':             keepSession(data.session); break;
    case 'logout':            dropSession(); break;
    case 'session_expired':   dropSession(); signIn(); break;
    case 'auth_required':     signIn(); break;
    case 'navigation':        syncTabs(data.path); break;
    case 'trade_opened':      track('trade_opened', data.position); break;
    case 'deposit_requested': openCashier(data.account, data.client_id); break;
    case 'open_external':     window.open(data.url, '_blank', 'noopener'); break;
    case 'unlock_requested':  confirmUser(function (ok) { send({ type: 'unlock', data: { ok: ok } }); }); break;
    case 'height':            frame.style.minHeight = data.height + 'px'; break;
    case 'error':             console.warn('module error', data.code, data.command || ''); break;
  }
});

Commands are only delivered once the frame has loaded; send them after the frame's load event or after ready. Nothing is sent by the module before the brand's embed switch is known; when the switch is off, one error { code: "embed_disabled" } arrives and the frame stays empty.

#Sign-in (SSO)

The page never holds the Partner API key. Your backend calls POST /api/external/v1/users/{external_id}/sso for the user who is signed in to your site and returns the one-time token; the page hands it to the module.

host.js - sign-in
// your backend: POST https://api.your-brand.com/api/external/v1/users/{external_id}/sso  -> { token, ... }
// the Partner API key stays on your server; the page only ever sees the one-time token
function signIn() {
  fetch('/app/trading-token', { credentials: 'include' })
    .then(function (r) { return r.json(); })
    .then(function (j) { send({ type: 'login', data: { token: j.token } }); });
}

// alternative for the very first load: the token on the URL
// frame.src = moduleUrl({ token: tokenFromYourBackend });
  • The token lives for the brand's login link lifetime (default 60 s) and works once; fetch it right before you send it.
  • The module answers login { session, client_id, account, email } - only when the origin option matches your page.
  • session_expired / auth_required: drop the kept session and sign in again with a fresh token.
  • Send logout when the user signs out of your site.
Never
log the token or the session, put them in a URL you share, or reuse a one-time token.

#Session restore

Keep session from the login event and hand it back with restoreSession on the next load: the user is signed in without a round trip through your backend. sessionStorage keeps it for the tab; use a stricter store if your page's threat model needs it, and never a cookie readable by other scripts.

host.js - restore
function keepSession(s) { sessionStorage.setItem('trading_session', s); }
function dropSession() { sessionStorage.removeItem('trading_session'); }

var kept = sessionStorage.getItem('trading_session');
frame.src = moduleUrl();
if (kept) {
  // after the frame loaded: hand the kept session over, no new token needed
  frame.addEventListener('load', function once() {
    frame.removeEventListener('load', once);
    send({ type: 'restoreSession', data: { session: kept } });
  });
} else {
  signIn();
}

#Events

typedataDo
ready{ version, protocol: 1, transport, screen, authenticated }Hide your splash; the module is up.
state{ authenticated, locked, gate, path, … }Answer to getState / resume.
error{ code, command?, message? }embed_disabled, bad_command, command_failed, unlock_denied.
login{ session, client_id, account, email }Keep session (only sent when origin matches your page).
logoutnullDrop the kept session.
session_expired{ path }Drop the session, fetch a new token.
auth_required{ path }Fetch a token.
navigation{ path }Sync your own URL or tabs.
trade_opened{ position, request }Analytics, in-page messages.
trade_closed{ position_id, partial, lots, position, bulk?, reason? }Analytics.
deposit_requested{ account, client_id }Open your cashier (deposit=host).
open_external{ url }Open in a new tab (links=host).
height{ height }Auto-size the frame.
unlock_requested{ reason }Confirm the user your way, answer unlock { ok }.
unlockednullThe lock screen is gone.
theme_applied{ mode?, primary?, bg?, paper?, text?, radius?, font? }What the module really applied.
push_registered{ ok, reason? }Not used by a web host.

Every event is { source: "qe-embed", v: 1, type, data, ts }. Payload details are in the embed guide.

#Commands

host.js - commands
send({ type: 'navigate',    data: { screen: 'wallet' } });              // terminal | wallet | history | account | positions
send({ type: 'navigate',    data: { path: '/dashboard/positions' } });
send({ type: 'setTheme',    data: { theme: 'light', primary: '#2563eb', radius: 10 } });
send({ type: 'setTheme',    data: null });                              // back to the brand's own theme
send({ type: 'setLanguage', data: { lang: 'de' } });
send({ type: 'lock' });                                                 // module answers unlock_requested
send({ type: 'unlock',      data: { ok: true } });
send({ type: 'getState' });                                             // module answers state
send({ type: 'logout' });
typedataEffect
login{ token, account? }One-time token - the module reloads and signs in.
restoreSession{ session, account? }A kept session - signed in without a new login.
logoutnullSign out.
setTheme{ theme, primary, bg, paper, text, radius, font }Any subset; null clears.
setLanguage{ lang }Switch the UI language.
navigate{ screen } or { path }Move to a screen or a module path.
locknullShow the lock screen.
unlock{ ok }Your confirmation result.
resumenullTab became visible again (document.visibilityState); re-checks the lock timer.
getStatenullAnswered with state.
initnullAnswered with state; not needed in a browser - the module initialises itself on load.
pushTokenRegistered{ token, platform, provider }Not used by a web host.

A failed command answers error { code: "command_failed", command }; an unknown one bad_command.

#Theming and language

  • Start values on the URL, runtime changes with setTheme (any subset, null clears). The module answers theme_applied with what it really applied.
  • Text colours are checked for 4.5:1 readability; an unreadable value is replaced by the module's own.
  • The browser's dark-mode preference does not override your theme.
  • setLanguage switches the UI; RTL languages (ar, fa) flip the layout automatically.
  • With hide=header,nav,branding (default) your page supplies the navigation; show=nav brings the module's own back.
host.js - your routes
// your own routes -> the module. Example: https://app.your-brand.com/trade/positions
var m = location.pathname.match(/^\/trade\/([a-z]+)/);
if (m) send({ type: 'navigate', data: { screen: m[1] } });

// keep your URL in step with the module
// case 'navigation': history.replaceState(null, '', '/trade' + data.path.replace('/dashboard', ''));

navigate takes a screen (terminal, wallet, history, account, positions) or a module path (/dashboard/…). The module reports every move with navigation { path }; mirror it into your own URL so the browser's back button and shared links behave.

host.js - sizing
// the module reports its document height; let the frame grow with it instead of scrolling inside
// case 'height': frame.style.minHeight = data.height + 'px';
// or give the frame the full viewport (the sample page) and let the module scroll itself

#Lock

With lock=1 the module locks after lock_after seconds while the tab is hidden, or on the lock command, and sends unlock_requested { reason }. A browser has no biometric prompt of its own: confirm the user your way (re-enter the site password, a passkey, a simple "Continue" if your page is already behind a login) and answer unlock { ok }. Send resume when the tab becomes visible again (visibilitychange) so the module re-checks the timer.

#Push

Not part of a web host: pushTokenRegistered and push_registered exist for the native SDKs. Trade and margin events for your own notifications come from the webhooks on your backend.

#KYC upload

The module's KYC page uses a normal file input. Give the frame allow="camera; microphone" so the browser lets it take an ID photo; without it the picker still offers files.

#Deposits and outside links

  • deposit=host: the Deposit button only sends deposit_requested { account, client_id } - open your cashier in your page; balances refresh by themselves. deposit=module keeps the platform cashier inside the frame.
  • links defaults to auto, which on an iframe means module: outside links open inside the frame. Set links=host to receive them as open_external { url } and open a new tab with noopener.

#Test plan

Run against the demo brand, in the browsers your clients use, before go-live:

  1. No session

    Load with no session: the neutral sign-in page shows and ready { authenticated: false } arrives.

  2. Token sign-in

    login { token } with a token from the demo Partner API: login arrives, the terminal shows, the token is not in the frame's URL. Reload the page: restoreSession signs in without a new token.

  3. Theme

    Dark / light, your primary colour, radius, font → theme_applied. Screenshots at 390 px wide.

  4. Hand-overs

    Deposit button → deposit_requested; with links=host an outside link → open_external.

  5. Lock

    Hide the tab for lock_after seconds (or send lock) → lock screen → your confirmation → unlocked.

  6. Frame allow-list

    The frame renders on your production origin and on your staging origin; a page on an origin that is not listed shows an empty frame.

  7. KYC

    A photo from the camera and a file from disk upload.

  8. Deep links

    Your /trade/<screen> route opens the right screen on a cold load and from a running page.

  9. Rough conditions

    Narrow viewport, the OS dark-mode switch (the module keeps your theme), offline and back online.

#Performance

  • The module is tuned for a throttled mobile profile (4× CPU slowdown, slow 4G).
  • One frame per page; keep it mounted while the user moves between your own views (hide it, do not remove it) and use navigate instead of reloading.
  • Give the frame its full height (the sample page) or follow the height event; avoid a scroll inside a scroll.

#Security notes

  • The module is framed only by origins on the brand's allow-list (CSP frame-ancestors).
  • Your listener drops every message whose ev.origin is not the module's origin or whose source is not qe-embed; every command is posted with the module's origin as the target, never *.
  • The login event carries the session only when the origin option names your page.
  • The one-time token is removed from the module's URL after the exchange; your page never stores it.
  • Open outside links with noopener.