Implementation

Complete working examples in Node.js, PHP, and Python. Copy one, wire it into your auth middleware, and you're live.

Prerequisites

  • A backend server (Node.js, PHP, Python, or any language with an HTTP client).
  • An HTTP client library (built-in fetch, curl, or requests).
  • A user authentication system - the proxy must know who the current user is.
  • Your WIDGET_API_KEY and WIDGET_COMMUNITY_ID from the Returning.AI dashboard.

Complete example

Each example implements the full four-step flow: authenticate, fetch HTML, inject token, serve. Pick your language.

server.js
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);

Environment variables

All examples read from environment variables. Create a .env file (and add it to .gitignore):

.env
# .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
VariableDescription
WIDGET_BASE_URLReturning.AI widget host. Defaults to https://prod-widgets.returning.ai
WIDGET_API_KEYYour secret API key. Never expose client-side.
WIDGET_COMMUNITY_IDIdentifies your community in the signin endpoint.
WIDGET_IDThe specific widget to render.
WIDGET_TYPEWidget type slug (e.g. store, channel).

Token injection

There are two ways to inject the JWT into the widget HTML. Use whichever matches how your widget reads its token.

Method 1: Replace in __WIDGET_INIT__

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}"`);

Method 2: localStorage script injection

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.