@returningai/widget-sdk
v1.6.1
Published
Shadow DOM isolated widget SDK for ReturningAI
Readme
@returningai/widget-sdk
Embed ReturningAI widgets on any website — React, Vue, Angular, or plain <script> tag. No external dependencies.
🛡️ 2026-05-11 security notice — refresh-token TTL for Access Key Embed. The auth backend now issues 0-TTL refresh tokens for Access Key Embed sessions, and rejects refresh tokens issued before this date. No SDK update required. See CHANGELOG.md for the full rationale and behavior changes.
Quick Start
Your access credentials stay on your server. Your server exchanges them for a short-lived embed token (15 min) and injects only that token into the page. No secret ever reaches the browser.
Step 1 — Create an access key pair (one-time, from your dashboard):
Go to Community Settings > Integrations > SDK Access and click Create SDK Key. Enter a name and the allowed origins for the sites that will embed the widget.
The dashboard will display your accessId and accessKey. The accessKey is shown once only — save it securely.
Store both values securely on your server.
Rotating or revoking keys: The same dashboard page lists existing keys and lets you revoke or replace them. Revocation takes effect immediately for token issuance, and forces active sessions to re-auth on their next page load.
Step 2 — Exchange credentials for an embed token on every page load (Node.js example):
// Runs on YOUR SERVER — never in the browser
const response = await fetch(
"https://api-v2.returning.ai/v2/api/widget-access-keys/token",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
accessId: process.env.RAI_ACCESS_ID,
accessKey: process.env.RAI_ACCESS_KEY,
// userIdentifiers keys MUST be `data-*` prefixed — they match the
// `dataAttribute` value of each field configured in your widget's
// User Identifier Fields (e.g. the dashboard stores "data-email",
// "data-user-id", not bare "email"/"userId").
userIdentifiers: {
"data-user-id": [YourUserObject].id,
"data-email": [YourUserObject].email,
// ...any other fields configured for this widget
},
}),
},
);
const { data } = await response.json();
// data.embedToken expires in 15 minutes and carries signed identity claimsStep 3 — Embed (only the short-lived token appears in HTML — no identity attributes):
<div
id="returning-ai-widget-YOUR_WIDGET_ID"
style="width:100%;height:600px"
></div>
<script
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
data-widget-id="YOUR_WIDGET_ID"
data-widget-type="store"
data-theme="dark"
data-embed-token="eyJ..."
widget-url="YOUR_WIDGET_URL"
></script>The SDK uses ReturningAI endpoints by default. api-url is optional and only needed when you want to override the widget auth server.
The server validates the token and extracts identity from its signed claims. If the token is expired, invalid, or its identifiers don't satisfy the widget's configured User Identifier Fields, the widget shows an error screen. Tokens expire after 15 minutes — regenerate on each page render.
Install
CDN (no install needed)
<script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"></script>npm / yarn / pnpm
npm install @returningai/widget-sdkWidget Types
Set the data-widget-type attribute to choose which widget to display. The Widget ID and Widget URL for each type are available in your dashboard.
| data-widget-type | Description |
| --------------------- | ---------------------- |
| store | Store / rewards widget |
| channel | Channel widget |
| milestone | Milestone widget |
| currency-view | Currency widget |
| referral-conditions | Referral widget |
| custom | Custom widget |
All examples below use Access Key Embed (recommended) — identity is signed into data-embed-token server-side per the Quick Start. Swap data-embed-token="eyJ..." for data-* identifier attributes if you want Public Embed instead.
Store widget
<div
id="returning-ai-widget-YOUR_WIDGET_ID"
style="width:100%;height:600px"
></div>
<script
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
data-widget-id="YOUR_WIDGET_ID"
data-widget-type="store"
data-theme="dark"
data-embed-token="eyJ..."
widget-url="YOUR_WIDGET_URL"
></script>Store callback field options
Store and custom-store widgets can ask the host page to populate dynamic purchase fields. In the store product setup, this applies to single-select custom instruction fields where Field Options is set to Callback Field. The callback returns signed field options; it is not a purchase eligibility check.
ReturningAI calls callbackFieldOptions with this payload:
{
scope: 'store.callback_field_options',
callbackName: 'callbackFieldOptions',
context: {
productId: 'PRODUCT_ID',
productName: 'Product name',
fields: [
{ id: 'FIELD_ID', name: 'Field label', type: 'single-select' },
],
},
}Register the callback on the widget element. For custom-store widgets, register it on the <rai-custom-widget> element instead.
async function registerStoreCallbacks() {
const widget = document.querySelector("rai-store-widget");
if (!widget) {
throw new Error("ReturningAI store widget was not found");
}
await customElements.whenDefined(widget.localName);
widget.registerCallback("callbackFieldOptions", async (payload) => {
// Call your backend. Never call ReturningAI's callback-signature endpoint
// directly from browser code because your SDK access key must stay private.
const response = await fetch("/api/returning-ai/store-field-options", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Unable to load store field options");
}
return response.json();
});
// Prevent later scripts from replacing this widget's registered callback.
widget.lockCallbacks();
}
registerStoreCallbacks().catch((error) => {
console.error("ReturningAI store callback setup failed", error);
});Your backend should generate options for each requested field, request a callback signature from ReturningAI, then return:
return {
fieldOptions: {
FIELD_ID: ["Option A", "Option B"],
},
signature: "SIGNATURE_FROM_CALLBACK_SIGNATURE_ENDPOINT",
};Node.js / Express example:
// Add this route to your backend. The browser callback above calls:
// POST /api/returning-ai/store-field-options
//
// This route receives ReturningAI's callback payload, builds options for each
// requested field, asks ReturningAI to sign the result, then returns
// { fieldOptions, signature } to the browser callback.
app.post("/api/returning-ai/store-field-options", async (req, res) => {
// The browser callback forwards ReturningAI's callback payload to this endpoint.
const payload = req.body;
// Build one option array for each requested dynamic field.
// Each key must be the field.id from payload.context.fields.
const fieldOptions = {};
for (const field of payload.context.fields) {
// Replace this with your own lookup using payload.context.productId and field.id.
fieldOptions[field.id] = ["Option 1", "Option 2"];
}
// Ask ReturningAI to sign the exact context and fieldOptions result.
// Keep SDK_ACCESS_ID and SDK_ACCESS_KEY on this backend only.
const signatureResponse = await fetch(
"https://api-v2.returning.ai/v2/api/widget-access-keys/callback-signature",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
accessId: process.env.SDK_ACCESS_ID,
accessKey: process.env.SDK_ACCESS_KEY,
scope: payload.scope,
callbackName: payload.callbackName,
context: payload.context,
result: { fieldOptions },
}),
},
);
if (!signatureResponse.ok) {
const errorText = await signatureResponse.text();
console.error("ReturningAI callback signature failed", errorText);
return res
.status(502)
.json({ error: "Unable to sign ReturningAI callback result" });
}
// Read the callback signature returned by ReturningAI.
const signaturePayload = await signatureResponse.json();
const signature = signaturePayload.data?.signature;
if (!signature) {
console.error(
"ReturningAI callback signature response did not include a signature",
signaturePayload,
);
return res
.status(502)
.json({ error: "Unable to read ReturningAI callback signature" });
}
// Return the signed options to the browser callback.
return res.json({
fieldOptions,
signature,
});
});Channel widget
<div
id="returning-ai-widget-YOUR_WIDGET_ID"
style="width:100%;height:600px"
></div>
<script
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
data-widget-id="YOUR_WIDGET_ID"
data-widget-type="channel"
data-theme="dark"
data-embed-token="eyJ..."
widget-url="YOUR_WIDGET_URL"
></script>Note:
YOUR_WIDGET_IDfor channel widgets is the base64-encoded channel ID — find it under Community Settings > Channels in your dashboard. Alternatively, use the custom element approach with separatecommunity-idandchannel-idattributes.
Milestone widget
<div
id="returning-ai-widget-YOUR_WIDGET_ID"
style="width:100%;height:600px"
></div>
<script
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
data-widget-id="YOUR_WIDGET_ID"
data-widget-type="milestone"
data-theme="dark"
data-embed-token="eyJ..."
widget-url="YOUR_WIDGET_URL"
></script>Custom widget
<div
id="returning-ai-widget-YOUR_WIDGET_ID"
style="width:100%;height:600px"
></div>
<script
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
data-widget-id="YOUR_WIDGET_ID"
data-widget-type="custom"
data-theme="dark"
data-embed-token="eyJ..."
widget-url="YOUR_WIDGET_URL"
></script>Custom widgets:
YOUR_WIDGET_IDis the base64-encoded_idof the CustomWidget record (not the community ID). The custom element variant<rai-custom-widget widget-id="...">uses this same base64 value — it's the one widget type where raw ObjectIds are not accepted.
Attributes
There are two ways to identify your widget:
- Script-tag embed: Use
data-widget-id(a pre-encoded ID from your dashboard) on the<script>tag - Custom element embed: Use
community-id(andchannel-idfor channel widgets) on the custom element tag
All attributes can be provided with or without the data- prefix — both work.
Script-tag attributes
| Attribute | Required | Default | Description |
| ------------------- | --------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| data-widget-id | Yes | — | Your widget ID (from dashboard) |
| data-widget-type | Yes | store | Widget type (see table above) |
| api-url | No | ReturningAI widget auth server | Optional widget auth server override |
| widget-url | Yes* | — | Widget URL (from dashboard). *Not required when using data-bundle-url |
| data-bundle-url | No | — | URL to the widget IIFE bundle — triggers bundle mode (non-iframe) |
| data-theme | No | light | light or dark |
| data-email | Public Embed only | — | User's email for identification. Access Key Embed: omit — identity is signed into data-embed-token |
| data-embed-token | Access Key Embed only | — | Short-lived JWT from your server. Carries signed user identifiers — no data-* identifier attributes needed |
| data-locale | No | — | BCP 47 locale tag (e.g. fr-FR) |
| data-eager | No | — | Load immediately instead of waiting until visible (presence-based boolean) |
| data-auto-refresh | No | true | Auto-refresh access token before expiry |
| data-debug | No | false | Verbose console logging |
| data-custom-data | No | — | JSON object forwarded to the widget |
| data-retry-label | No | Retry | Label text for the retry button on the error screen |
| data-v2-api-url | No | ReturningAI V2 API | Optional V2 API override for bundle mode |
Custom element attributes
| Attribute | Required | Default | Description |
| -------------- | --------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| community-id | Yes* | — | Your community ID (raw ObjectId from dashboard). *Custom widgets use widget-id instead |
| channel-id | Channel only | — | Channel ID (raw ObjectId) — required for <rai-channel-widget> |
| api-url | No | ReturningAI widget auth server | Optional widget auth server override |
| widget-url | Yes* | — | Widget URL (from dashboard). *Not required when using bundle-url |
| bundle-url | No | — | URL to the widget IIFE bundle — triggers bundle mode (non-iframe) |
| theme | No | light | light or dark |
| data-email | Public Embed only | — | User's email for identification. Access Key Embed: omit — identity is signed into embed-token |
| embed-token | Access Key Embed only | — | Short-lived JWT from your server. Carries signed user identifiers — no data-* identifier attributes needed |
| width | No | 100% | CSS width |
| height | No | 600px | CSS height |
| locale | No | — | BCP 47 locale tag (e.g. fr-FR) |
| eager | No | — | Load immediately instead of waiting until visible (presence-based boolean) |
| auto-refresh | No | true | Auto-refresh access token before expiry |
| debug | No | false | Verbose console logging |
| custom-data | No | — | JSON object forwarded to the widget |
| retry-label | No | Retry | Label text for the retry button on the error screen |
| v2-api-url | No | ReturningAI V2 API | Optional V2 API override for bundle mode |
widget-idvscommunity-id: The script-tag method usesdata-widget-idwhich is a pre-encoded identifier (base64). The custom element method uses raw ObjectIds —community-idfor most widget types, pluschannel-idfor channel widgets. The SDK handles the encoding internally.
Container <div> (script-tag method only)
The SDK looks for a <div> with id="returning-ai-widget-{YOUR_WIDGET_ID}". Size the div to control the widget dimensions:
<div
id="returning-ai-widget-YOUR_WIDGET_ID"
style="width:100%;height:600px"
></div>Custom elements don't need a container div — they size themselves using width and height attributes (defaults: 100% and 600px).
User Identifier Attributes
Access Key Embed: Do not put identifiers in HTML. Pass them inside the
userIdentifiersobject when exchanging your access credentials for an embed token (Step 2). Identity is cryptographically signed into the token so it cannot be tampered with from the browser.Public Embed: Pass any
data-*attribute to identify the current user. Which identifiers are available is configured per community in your dashboard.
<!-- Public Embed — identifiers in HTML -->
<script
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
data-widget-id="YOUR_WIDGET_ID"
data-widget-type="store"
data-email="[email protected]"
data-user-id="12345"
data-member-id="abc"
widget-url="YOUR_WIDGET_URL"
></script>In Public Embed mode, any data-* attribute not reserved by the SDK is forwarded to the auth API as a user identifier.
Endpoint URL Overrides
The SDK uses ReturningAI endpoints by default.
These fields are optional. Set them only when you need to override the default endpoints:
api-url/data-api-url— overrides the widget auth server used for SDK auth, refresh, logout, and error settings.v2-api-url/data-v2-api-url— overrides the V2 API base URL used by bundle mode.
These endpoint overrides do not replace widget-url or bundle-url; use the widget and bundle URLs from your dashboard.
Bundle Mode
When you set the bundle-url attribute, the widget renders directly in the page DOM instead of inside an iframe. This gives full CSS cascade — html[data-theme] and Tailwind classes apply to the widget content naturally.
widget-urlis not needed in bundle mode (the widget is loaded directly frombundle-url).- ReturningAI endpoints are used by default.
api-urlandv2-api-urlare optional endpoint URL overrides.
Both examples below use Access Key Embed and the default endpoints.
<!-- Script-tag approach -->
<div
id="returning-ai-widget-YOUR_WIDGET_ID"
style="width:100%;height:100vh"
></div>
<script
src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"
data-widget-id="YOUR_WIDGET_ID"
data-widget-type="store"
data-theme="dark"
data-embed-token="eyJ..."
data-bundle-url="YOUR_BUNDLE_URL"
data-eager
></script>
<!-- Custom element approach -->
<rai-store-widget
community-id="YOUR_COMMUNITY_ID"
bundle-url="YOUR_BUNDLE_URL"
embed-token="eyJ..."
theme="dark"
width="100%"
height="100vh"
eager
></rai-store-widget>Each widget type has its own IIFE bundle and global name:
| Widget type | Bundle global | Custom element tag |
| --------------------- | -------------------- | ------------------------ |
| store | RaiStoreWidget | <rai-store-widget> |
| channel | RaiChannelWidget | <rai-channel-widget> |
| milestone | RaiMilestoneWidget | <rai-milestone-widget> |
| currency-view | RaiCurrencyWidget | <rai-currency-widget> |
| referral-conditions | RaiReferralWidget | <rai-referral-widget> |
| custom | RaiCustomWidget | <rai-custom-widget> |
The bundle must export a mount(container, config) function on its global (e.g. window.RaiStoreWidget.mount).
Framework Usage (Custom Elements)
When using a frontend framework (React, Vue, Angular), you can use custom HTML element tags instead of the script-tag method. Custom elements use raw ObjectIds (community-id, channel-id) instead of the pre-encoded data-widget-id — the SDK handles the encoding internally. (Exception: <rai-custom-widget> uses widget-id with a base64-encoded value — see the attributes table.)
All examples below use Access Key Embed — pass the short-lived token your server minted into embed-token. For Public Embed, replace embed-token with data-* identifier attributes.
Plain HTML
<script src="https://unpkg.com/@returningai/widget-sdk/dist/rai-widget.iife.js"></script>
<!-- Store widget -->
<rai-store-widget
community-id="YOUR_COMMUNITY_ID"
theme="dark"
embed-token="eyJ..."
widget-url="YOUR_WIDGET_URL"
></rai-store-widget>
<!-- Channel widget — requires both community-id AND channel-id -->
<rai-channel-widget
community-id="YOUR_COMMUNITY_ID"
channel-id="YOUR_CHANNEL_ID"
theme="dark"
embed-token="eyJ..."
widget-url="YOUR_WIDGET_URL"
></rai-channel-widget>React (TypeScript)
Types are included — no manual JSX declarations needed.
import "@returningai/widget-sdk";
export function StoreEmbed({ embedToken }: { embedToken: string }) {
return (
<rai-store-widget
community-id="YOUR_COMMUNITY_ID"
theme="dark"
embed-token={embedToken}
widget-url="YOUR_WIDGET_URL"
/>
);
}
export function ChannelEmbed({ embedToken }: { embedToken: string }) {
return (
<rai-channel-widget
community-id="YOUR_COMMUNITY_ID"
channel-id="YOUR_CHANNEL_ID"
theme="dark"
embed-token={embedToken}
widget-url="YOUR_WIDGET_URL"
/>
);
}For callback-backed store fields, register the callback through a ref:
import { useEffect, useRef } from "react";
import "@returningai/widget-sdk";
import type {
ReturningAIStoreCallbackFieldOptionsCallback,
ReturningAIWidgetCallbackRegistration,
} from "@returningai/widget-sdk";
type ReturningAIStoreElement = HTMLElement &
ReturningAIWidgetCallbackRegistration;
const fieldOptionsCallback: ReturningAIStoreCallbackFieldOptionsCallback =
async (payload) => {
const response = await fetch("/api/returning-ai/store-field-options", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Unable to load ReturningAI store field options");
}
return response.json();
};
export function StoreEmbedWithCallbacks({
embedToken,
}: {
embedToken: string;
}) {
const storeWidgetRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const storeWidget =
storeWidgetRef.current as ReturningAIStoreElement | null;
if (!storeWidget) {
return;
}
storeWidget.registerCallback("callbackFieldOptions", fieldOptionsCallback);
// Prevent later scripts from replacing this widget's registered callback.
storeWidget.lockCallbacks();
}, []);
return (
<rai-store-widget
ref={storeWidgetRef}
community-id="YOUR_COMMUNITY_ID"
theme="dark"
embed-token={embedToken}
widget-url="YOUR_WIDGET_URL"
/>
);
}Vue 3
<script setup>
import "@returningai/widget-sdk";
defineProps({ embedToken: String });
</script>
<template>
<!-- Store widget -->
<rai-store-widget
community-id="YOUR_COMMUNITY_ID"
theme="dark"
:embed-token="embedToken"
widget-url="YOUR_WIDGET_URL"
/>
<!-- Channel widget -->
<rai-channel-widget
community-id="YOUR_COMMUNITY_ID"
channel-id="YOUR_CHANNEL_ID"
theme="dark"
:embed-token="embedToken"
widget-url="YOUR_WIDGET_URL"
/>
</template>For callback-backed store fields, register the callback through a template ref:
<script setup lang="ts">
import { onMounted, ref } from "vue";
import "@returningai/widget-sdk";
import type {
ReturningAIStoreCallbackFieldOptionsCallback,
ReturningAIWidgetCallbackRegistration,
} from "@returningai/widget-sdk";
const props = defineProps<{ embedToken: string }>();
const storeWidget = ref<
(HTMLElement & ReturningAIWidgetCallbackRegistration) | null
>(null);
const fieldOptionsCallback: ReturningAIStoreCallbackFieldOptionsCallback =
async (payload) => {
const response = await fetch("/api/returning-ai/store-field-options", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Unable to load ReturningAI store field options");
}
return response.json();
};
onMounted(() => {
if (storeWidget.value) {
storeWidget.value.registerCallback(
"callbackFieldOptions",
fieldOptionsCallback,
);
// Prevent later scripts from replacing this widget's registered callback.
storeWidget.value.lockCallbacks();
}
});
</script>
<template>
<rai-store-widget
ref="storeWidget"
community-id="YOUR_COMMUNITY_ID"
theme="dark"
:embed-token="props.embedToken"
widget-url="YOUR_WIDGET_URL"
/>
</template>Angular
// app.module.ts
import '@returningai/widget-sdk'
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })<!-- Store widget -->
<rai-store-widget
community-id="YOUR_COMMUNITY_ID"
theme="dark"
[attr.embed-token]="embedToken"
widget-url="YOUR_WIDGET_URL"
></rai-store-widget>
<!-- Channel widget -->
<rai-channel-widget
community-id="YOUR_COMMUNITY_ID"
channel-id="YOUR_CHANNEL_ID"
theme="dark"
[attr.embed-token]="embedToken"
widget-url="YOUR_WIDGET_URL"
></rai-channel-widget>For callback-backed store fields, add a template ref and register the callback in the component:
import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core";
import type {
ReturningAIStoreCallbackFieldOptionsCallback,
ReturningAIWidgetCallbackRegistration,
} from "@returningai/widget-sdk";
type ReturningAIStoreElement = HTMLElement &
ReturningAIWidgetCallbackRegistration;
@Component({
selector: "app-store-widget",
templateUrl: "./store-widget.component.html",
})
export class StoreWidgetComponent implements AfterViewInit {
@ViewChild("storeWidget") storeWidget!: ElementRef<ReturningAIStoreElement>;
embedToken = "TOKEN_FROM_YOUR_SERVER";
ngAfterViewInit() {
const fieldOptionsCallback: ReturningAIStoreCallbackFieldOptionsCallback =
async (payload) => {
const response = await fetch("/api/returning-ai/store-field-options", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Unable to load ReturningAI store field options");
}
return response.json();
};
this.storeWidget.nativeElement.registerCallback(
"callbackFieldOptions",
fieldOptionsCallback,
);
// Prevent later scripts from replacing this widget's registered callback.
this.storeWidget.nativeElement.lockCallbacks();
}
}<rai-store-widget
#storeWidget
community-id="YOUR_COMMUNITY_ID"
theme="dark"
[attr.embed-token]="embedToken"
widget-url="YOUR_WIDGET_URL"
></rai-store-widget>Available custom element tags
| Tag | Required attributes | Widget type |
| ------------------------ | -------------------------------- | ----------- |
| <rai-store-widget> | community-id | Store |
| <rai-channel-widget> | community-id + channel-id | Channel |
| <rai-milestone-widget> | community-id | Milestone |
| <rai-currency-widget> | community-id | Currency |
| <rai-referral-widget> | community-id | Referral |
| <rai-custom-widget> | widget-id (not community-id) | Custom |
DOM Events
Listen for widget lifecycle events:
| Event | detail | Fired when |
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| rai-authenticated | {} | Auth succeeded, before widget mounts |
| rai-ready | {} | WIDGET_READY received, loader hidden (iframe mode) |
| rai-mounted | {} | Widget bundle mounted successfully (bundle mode) |
| rai-error | { message, hint?, status? } | Auth failed after all retries. message is the backend error text (e.g. "Invalid or expired embed token"), hint is the optional human-readable suggestion, status is the HTTP status code of the failing response. |
| rai-logout | {} | Widget logged out |
| rai-height-change | { height } | iframe resized (after debounce) |
// Script-tag embed
const widget = document.querySelector("[data-widget-id]");
// Custom element embed
const widget = document.querySelector("rai-store-widget");
widget.addEventListener("rai-authenticated", () => {
/* auth succeeded */
});
widget.addEventListener("rai-ready", () => {
/* widget loaded and visible (iframe mode) */
});
widget.addEventListener("rai-mounted", () => {
/* widget mounted (bundle mode) */
});
widget.addEventListener("rai-error", (e) =>
console.error(e.detail.status, e.detail.message, e.detail.hint),
);
widget.addEventListener("rai-logout", () => {
/* user logged out */
});
widget.addEventListener("rai-height-change", (e) =>
console.log(e.detail.height),
);Public API
After the SDK loads, window.ReturningAIWidget is available:
window.ReturningAIWidget.version; // current SDK version
await window.ReturningAIWidget.reload(); // re-runs auth flow and reloads widget
await window.ReturningAIWidget.logout(); // clears tokens and removes widget
window.ReturningAIWidget.isAuthenticated(); // boolean
window.ReturningAIWidget.getTokenInfo(); // token metadata (no token values exposed)Browser Support
Requires Custom Elements v1 + Shadow DOM v1.
| Browser | Minimum version | | ------- | --------------- | | Chrome | 67 | | Firefox | 63 | | Safari | 13 |
Banner SDK (<rai-banner>)
A separate, lightweight runtime for embedding ReturningAI banners on any site —
no auth, no iframe. Ships as its own bundle, dist/rai-banner.iife.js (~13 KB gzip).
Widgets render banners automatically. As of 1.6.0, mounting any widget (
<rai-store-widget>,<rai-channel-widget>, …) also renders eligible banners targeted at that widget type — no extra markup, and the widget element emits the samerai-banner-*events. Disable per widget withbanner="off". The<rai-banner>element below is for embedding banners on pages with no widget.
Embed
Custom element:
<script src="https://unpkg.com/@returningai/[email protected]/dist/rai-banner.iife.js"></script>
<rai-banner community-id="YOUR_COMMUNITY_ID" domain-key="GEN"></rai-banner>Or script-tag bootstrap (auto-creates the element):
<script
src="https://unpkg.com/@returningai/[email protected]/dist/rai-banner.iife.js"
data-rai-banner
data-community-id="YOUR_COMMUNITY_ID"
data-domain-key="GEN"
></script>Attributes
| Attribute | Required | Description |
| ------------------------------------------ | -------- | ------------------------------------------------------------------- |
| community-id | yes | Community ObjectId |
| api-url | no | Explicit API base (overrides domain-key) |
| domain-key | no | LOCAL | SGTR | STG | GEN (default LOCAL) |
| surface | no | Defaults to sdk |
| page-path / page-hostname / page-url | no | Override page context (default: window.location) for page rules |
| storage-prefix | no | localStorage key prefix for the anonymous visitor id |
| embed-token | no* | SDK embed token (minted from your access id/key, like the widgets). Required for banners whose Access is domain or authenticated — see below |
How it works
- Mints/loads an anonymous visitor UUID in
localStorage(anonymous_session). - Calls
POST {api}/v2/api/banner-sdk/eligiblefor thesdksurface with page + viewport context. - Renders the returned banner —
overlay/popupplacement → a centered card (portaled to<body>);inline→ into the configured target selector, or — when that selector isn't on the page — in place where the<rai-banner>element sits. No mount<div>is required for any placement: overlay/popup float over the page, inline renders where the snippet/element is. - Fires signed impression / click / dismiss tracking URLs (one-time, HMAC-signed, replay-protected server-side).
- Banner HTML is sanitized with DOMPurify before injection.
Access types — each banner has an Access setting (chosen in the dashboard) that decides what the embed must provide:
| Banner Access | What the embed needs |
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Open to everyone | nothing — community-id only |
| Domain gate | the page is served from one of the banner's allowed domains (enforced on the request Origin), or an embed-token whose access key authorizes the origin |
| SDK auth | a valid embed-token whose access key authorizes the origin (mint it from your access id/key, exactly like widgets) |
A live banner whose surfaces include sdk is required. Widget‑mounted banners pass the widget's embed-token
automatically, so domain / authenticated banners work on widgets with no extra setup.
Events
The element emits bubbling CustomEvents:
| Event | Detail |
| ----------------------- | --------------------------------- |
| rai-banner-impression | { bannerId } |
| rai-banner-click | { bannerId } |
| rai-banner-dismiss | { bannerId } |
| rai-banner-empty | { reason } (no eligible banner) |
| rai-banner-error | { error } |
document.addEventListener('rai-banner-impression', (e) => console.log('shown', e.detail.bannerId))Build
npm run build:banner # dev
npm run build:banner:min # production (minified)