npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 claims

Step 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-sdk

Widget 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_ID for channel widgets is the base64-encoded channel ID — find it under Community Settings > Channels in your dashboard. Alternatively, use the custom element approach with separate community-id and channel-id attributes.

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_ID is the base64-encoded _id of 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:

  1. Script-tag embed: Use data-widget-id (a pre-encoded ID from your dashboard) on the <script> tag
  2. Custom element embed: Use community-id (and channel-id for 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-id vs community-id: The script-tag method uses data-widget-id which is a pre-encoded identifier (base64). The custom element method uses raw ObjectIds — community-id for most widget types, plus channel-id for 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 userIdentifiers object 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-url is not needed in bundle mode (the widget is loaded directly from bundle-url).
  • ReturningAI endpoints are used by default.
  • api-url and v2-api-url are 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 same rai-banner-* events. Disable per widget with banner="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

  1. Mints/loads an anonymous visitor UUID in localStorage (anonymous_session).
  2. Calls POST {api}/v2/api/banner-sdk/eligible for the sdk surface with page + viewport context.
  3. Renders the returned banner — overlay/popup placement → 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.
  4. Fires signed impression / click / dismiss tracking URLs (one-time, HMAC-signed, replay-protected server-side).
  5. 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)