Referral Widget
Embed the Returning.AI referral experience in a logged-in portal using the Widget SDK, Access Key Embed, and the <rai-referral-widget> web component.
Keep the two identifiers aligned
The referral widget token and the registration webhook must use the same identifier for the same trader. If the token uses userIdentifiers: { 'data-customer-id': user.id }, send that same ID as customerId in the registration payload.
Flow
- 1. Register the user. Your backend sends a registration event to Returning.AI after signup or account confirmation. Use the handshake URL first, then send the main webhook request with
x-session-token. - 2. Mint the widget token. Your backend exchanges
accessId/accessKeyfor a short-livedembed-token. Put the user identifier insideuserIdentifiers, not on the widget tag. - 3. Render the widget. The frontend renders
<rai-referral-widget>with the returned token and thereferral-conditions-widgetbundle URL.
Register Users Server-Side
Registration events belong on your backend. Use the workflow API key only on the handshake request. The main webhook call should use the returned sessionToken in the x-session-token header.
async function sendRegistrationToReturningAI(user) {
const handshakeResponse = await fetch(
process.env.RAI_REGISTRATION_HANDSHAKE_URL,
{
method: 'POST',
headers: {
Authorization: process.env.RAI_REGISTRATION_WEBHOOK_API_KEY,
'Content-Type': 'application/json',
},
}
)
if (!handshakeResponse.ok) {
throw new Error('Returning.AI registration handshake failed')
}
const handshakeBody = await handshakeResponse.json()
const sessionToken = handshakeBody.data?.sessionToken
if (!sessionToken) {
throw new Error('Returning.AI registration handshake returned no session token')
}
const webhookResponse = await fetch(
process.env.RAI_REGISTRATION_WEBHOOK_URL,
{
method: 'POST',
headers: {
'x-session-token': sessionToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerId: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
registeredAt: new Date().toISOString(),
}),
}
)
if (!webhookResponse.ok) {
throw new Error('Returning.AI registration webhook failed')
}
}Mint the Referral Embed Token
The access key stays server-side. Prefer a stable broker-owned ID such as data-customer-id over email unless email is your canonical mapping key.
app.get('/api/returningai/referral-token', async (req, res) => {
const user = req.user
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': user.id,
},
}),
}
)
if (!response.ok) {
return res.status(502).json({ error: 'Failed to mint embed token' })
}
const result = await response.json()
res.json({ embedToken: result.data.embedToken })
})Render the Widget
Use embed-token, not data-embed-token. Use data-embed-token only on SDK script-tag embeds. Do not put data-customer-id or data-email directly on the widget for Access Key Embed.
<rai-referral-widget
community-id="YOUR_COMMUNITY_ID"
embed-token="TOKEN_FROM_YOUR_BACKEND"
bundle-url="https://prod-widgets.returning.ai/referral-conditions-widget/bundle/widget.js"
theme="dark"
width="100%"
height="600px"
></rai-referral-widget>React example
import { useEffect, useRef, useState } from 'react'
import '@returningai/widget-sdk'
export function ReferralPage() {
const widgetRef = useRef<HTMLElement | null>(null)
const [embedToken, setEmbedToken] = useState<string | null>(null)
async function loadEmbedToken() {
const response = await fetch('/api/returningai/referral-token')
if (!response.ok) throw new Error('Failed to load Returning.AI token')
const result = await response.json()
setEmbedToken(result.embedToken)
return result.embedToken
}
useEffect(() => {
loadEmbedToken().catch(console.error)
}, [])
useEffect(() => {
const widget = widgetRef.current
if (!widget) return
const handleError = async () => {
const freshToken = await loadEmbedToken()
widget.setAttribute('embed-token', freshToken)
await window.ReturningAIWidget?.reload?.()
}
widget.addEventListener('rai-error', handleError)
return () => widget.removeEventListener('rai-error', handleError)
}, [])
if (!embedToken) return null
return (
<rai-referral-widget
ref={widgetRef}
community-id="YOUR_COMMUNITY_ID"
embed-token={embedToken}
bundle-url="https://prod-widgets.returning.ai/referral-conditions-widget/bundle/widget.js"
theme="dark"
width="100%"
height="600px"
/>
)
}TypeScript JSX Declaration
If TypeScript does not recognize the custom element, add a declaration file such as src/types/returningai-widget.d.ts.
import type * as React from 'react'
type ReturningAIReferralWidgetProps =
React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
'community-id'?: string
'embed-token'?: string
'bundle-url'?: string
theme?: string
width?: string
height?: string
}
declare global {
namespace JSX {
interface IntrinsicElements {
'rai-referral-widget': ReturningAIReferralWidgetProps
}
}
}
export {}Checklist
- Use the same identifier in registration and in the token mint request.
- Keep access keys and workflow API keys on the backend only.
- Use the registration handshake before calling the main webhook.
- Use the slug
referral-conditions-widgetin the bundle URL. - Refresh long-lived widget sessions on the
rai-errorevent.
For the generic Access Key flow, see Auth - Access Key.