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.
Keep credentials server-side
Never include RAI_ACCESS_ID or RAI_ACCESS_KEY in client-side code. They should only exist in environment variables on your backend.
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.
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.
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),
),
);
}
}Allow the exact WebView origin
Domain checks use the origin loaded inside the WebView. http://localhost:3000, http://127.0.0.1:3000, http://10.0.2.2:3000, and https://portal.example.com are different origins. Desktop localhost may work in development mode, but Android emulator testing usually reaches the host machine through 10.0.2.2, which may need to be allowed separately.
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 userIdentifiers | Typical source | When to use it |
|---|---|---|
| data-customer-id | Broker CRM / platform customer ID | Preferred for most production integrations. Stable, broker-owned, and avoids exposing email. |
| data-user-id | Your internal user ID | Good fallback if that field already exists in your platform and matches the ID used during registration. |
| data-email | User email | Use only when email is already your canonical identifier or you do not have a better stable ID. |
| data-user-objectid | Returning.AI object ID | Only 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.
Keep identifiers in sync with registration
The key and value you sign into userIdentifiers must match the identifier you sent when the user registered via the Registration webhook. If they diverge, the widget will not find the user's loyalty profile.
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.
| Scenario | Strategy |
|---|---|
| Server-rendered pages | Token generated per page load. 15 minutes is plenty for most page sessions. |
| SPAs | Re-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:
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()
}
})