Skip to content
Returning.AIDocs

Auth - Access Key

The recommended way to authenticate widgets. Your backend exchanges access credentials for a short-lived JWT, which is injected into the widget tag as an HTML attribute. User identity is signed into that token request server-side, so no browser-visible user identifiers are needed on the widget tag. No persistent auth endpoint needed.

When to use

  • Logged-in portals and trader dashboards where you know the user server-side.
  • Any integration where server-verified identity is important.
  • Bundle mode (native DOM rendering) for the best performance and CSS integration.

Step 1 - Get access credentials

In the Returning.AI admin panel, generate an accessId and accessKey pair. Store them in your server's environment variables - they must never be exposed to the client.

Step 2 - Create a backend endpoint

Your server calls the Access Key API with your accessId and accessKey, plus a userIdentifiers object containing the fields your widget expects. The response returns a short-lived embed token at data.embedToken. Your browser only receives that token.

server.js
app.get('/api/widget-token', async (req, res) => {
  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: {
          'data-customer-id': req.user.customerId,
        },
      }),
    }
  )
  const json = await response.json()
  const embedToken = json.data?.embedToken
  if (!embedToken) throw new Error('Missing embed token')

  res.json({ embedToken })
})

Response shape

A successful call returns the JWT under data.embedToken and the lifetime in seconds under data.expiresIn.

// POST https://api-v2.returning.ai/v2/api/widget-access-keys/token
// Request body: { accessId, accessKey, userIdentifiers }
// Response body (200 OK):

{
  "data": {
    "embedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": 900
  }
}

// Notes:
// - embedToken: short-lived JWT, pass to the widget as the embed-token attribute
// - expiresIn: token lifetime in seconds (typically 900 = 15 minutes)
// - For long-lived views, mint a fresh token before expiry and update the
//   embed-token attribute on the widget element. SDK 1.4.2+ watches the
//   attribute; call reload() only as a fallback for older builds.

Step 3 - Inject the token into the page

Your server renders the widget tag with the embed-token attribute set to the token from Step 2. The SDK validates that token in the browser as an access gate, then continues startup using the signed identity claims from the token mint request.

Step 4 - Add the widget

Add the widget tag with embed-token and bundle-url attributes. The bundle-url enables bundle mode (recommended) where the widget renders directly in the page DOM instead of an iframe.

<rai-store-widget
  community-id="YOUR_COMMUNITY_ID"
  embed-token="TOKEN_FROM_YOUR_SERVER"
  bundle-url="https://prod-widgets.returning.ai/store-widget/bundle/widget.js"
  theme="dark"
  width="100%"
  height="600px"
></rai-store-widget>

Bundle mode

When bundle-url is set, the widget renders in the light DOM (no iframe). Your page CSS cascades into the widget, scrolling is native, and performance is better. Omit bundle-url to fall back to iframe mode if you need CSS isolation.

Mobile apps and WebViews

The Widget SDK is browser-based Web Component code. Do not add the SDK directly to a native iOS, Android, or Flutter screen. For mobile apps, host the widget on a normal web page, then load that page inside a WebView.

If your portal already has a page where the widget works in a browser, use that same page as the WebView URL. Keep the access key and token minting on your backend. The Flutter app should only open the page that receives the short-lived embed-token from your server.

returning_widget_page.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class ReturningWidgetPage extends StatefulWidget {
  const ReturningWidgetPage({super.key});

  @override
  State<ReturningWidgetPage> createState() => _ReturningWidgetPageState();
}

class _ReturningWidgetPageState extends State<ReturningWidgetPage> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();

    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..loadRequest(
        Uri.parse('https://portal.example.com/loyalty/widget'),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: WebViewWidget(controller: controller),
      ),
    );
  }
}

User identifier keys

The browser should only receive embed-token. The user identifier keys themselves are sent server-side in the userIdentifiers object when you mint the token:

Key in userIdentifiersTypical sourceWhen to use it
data-customer-idBroker CRM / platform customer IDPreferred for most production integrations. Stable, broker-owned, and avoids exposing email.
data-user-idYour internal user IDGood fallback if that field already exists in your platform and matches the ID used during registration.
data-emailUser emailUse only when email is already your canonical identifier or you do not have a better stable ID.
data-user-objectidReturning.AI object IDOnly use if you already persist the Returning.AI-side user object ID and explicitly want to key off it.

Which identifiers are required depends on your community's widget configuration. Check your admin panel, or ask your Returning.AI contact, for the exact required key before rolling out. Missing a required identifier in the server-side userIdentifiers object causes the widget to fail on auth with a rai-error event.

Token lifecycle

Access Key tokens are short-lived JWTs that expire after 15 minutes. The SDK does not call your backend to mint a fresh embed-token automatically. For long-lived views, your app must fetch a new embed token, then update the embed-token attribute. SDK 1.4.2+ watches that attribute and reinitializes the widget; call reload() only as an older-build fallback.

ScenarioStrategy
Server-rendered pagesToken generated per page load. 15 minutes is plenty for most page sessions.
SPAsRe-fetch the token from your backend on route navigation. Each view gets a fresh token.
Long-lived sessions (>15 min on one view)Listen for the rai-error event, re-fetch a token from your backend, update the embed-token attribute. SDK 1.4.2+ detects the change; call window.ReturningAIWidget.reload() only as an older-build fallback.

Handling token expiry in long-lived sessions

If a user stays on the same page for more than 15 minutes, the token will expire. Use this pattern to recover automatically:

token-refresh.js
const widget = document.querySelector('rai-store-widget')

widget.addEventListener('rai-error', async (e) => {
  console.warn('Widget token expired, refreshing...')

  // Re-fetch a fresh token from your backend
  const res = await fetch('/api/widget-token')
  const { embedToken } = await res.json()

  // SDK 1.4.2+ watches this attribute and reinitializes.
  widget.setAttribute('embed-token', embedToken)

  // Compatibility fallback for older SDK builds.
  if (window.ReturningAIWidget?.reload) {
    await window.ReturningAIWidget.reload()
  }
})