Complete working examples in Node.js, PHP, and Python. Copy one, wire it into your auth middleware, and you're live.
fetch, curl, or requests).WIDGET_API_KEY and WIDGET_COMMUNITY_ID from the Returning.AI dashboard.Each example implements the full four-step flow: authenticate, fetch HTML, inject token, serve. Pick your language.
const express = require('express');
const app = express();
const WIDGET_BASE_URL = process.env.WIDGET_BASE_URL || 'https://prod-widgets.returning.ai';
const WIDGET_API_KEY = process.env.WIDGET_API_KEY;
const WIDGET_COMMUNITY_ID = process.env.WIDGET_COMMUNITY_ID;
const WIDGET_ID = process.env.WIDGET_ID;
const WIDGET_TYPE = process.env.WIDGET_TYPE || 'store';
const requireAuth = (req, res, next) => {
if (!req.user) return res.status(401).json({ error: 'Authentication required' });
next();
};
app.get('/widget', requireAuth, async (req, res) => {
try {
const userEmail = req.user.email;
// Step 1: Fetch JWT token
const tokenResponse = await fetch(
`${WIDGET_BASE_URL}/widget/${WIDGET_COMMUNITY_ID}/signin`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'returningai-api-key': WIDGET_API_KEY,
'email': userEmail
}
}
);
if (!tokenResponse.ok) {
return res.status(tokenResponse.status).json({ error: 'Unable to authenticate widget' });
}
const { token } = await tokenResponse.json();
// Step 2: Fetch widget HTML
const htmlResponse = await fetch(
`${WIDGET_BASE_URL}/${WIDGET_TYPE}/${WIDGET_ID}?color=light`
);
let html = await htmlResponse.text();
// Step 3: Inject token
const safeToken = token.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
html = html.replace(/token:\s*""/, `token: "${safeToken}"`);
// Step 4: Serve
res.setHeader('Content-Type', 'text/html');
res.setHeader('Cache-Control', 'no-store');
res.send(html);
} catch (error) {
console.error('Widget error:', error);
res.status(500).json({ error: 'Unable to load widget' });
}
});
app.listen(3000);All examples read from environment variables. Create a .env file (and add it to .gitignore):
# .env - never commit this file
WIDGET_BASE_URL=https://prod-widgets.returning.ai
WIDGET_API_KEY=rai_live_xxxxxxxxxxxxxxxxxxxx
WIDGET_COMMUNITY_ID=cm1abc2de3fg4hi5jk
WIDGET_ID=cm9xyz8wv7ut6sr5qp
WIDGET_TYPE=store| Variable | Description |
|---|---|
WIDGET_BASE_URL | Returning.AI widget host. Defaults to https://prod-widgets.returning.ai |
WIDGET_API_KEY | Your secret API key. Never expose client-side. |
WIDGET_COMMUNITY_ID | Identifies your community in the signin endpoint. |
WIDGET_ID | The specific widget to render. |
WIDGET_TYPE | Widget type slug (e.g. store, channel). |
There are two ways to inject the JWT into the widget HTML. Use whichever matches how your widget reads its token.
The widget HTML contains a window.__WIDGET_INIT__ object with an empty token field. Replace the empty string with the real JWT.
// Method 1: Replace __WIDGET_INIT__ token placeholder
// The widget HTML contains: window.__WIDGET_INIT__ = { token: "", ... }
html = html.replace(/token:\s*""/, `token: "${safeToken}"`);If the widget reads its token from localStorage, inject a script that sets the token before the widget initializes.
// Method 2: Inject a localStorage script before </body>
// Use this if the widget reads its token from localStorage.
const script = `<script>localStorage.setItem('returning-ai-widget-token','${safeToken}');</script>`;
html = html.replace('</body>', script + '</body>');Cache-Control: no-store
Use no-store (not no-cache) to prevent caching of authenticated widget pages. no-cache still stores a copy and revalidates, which means a stale token could be served from a shared cache or CDN. no-store guarantees the response is never written to disk.