Referrals
Referral attribution has separate checkpoints: capture the incoming value, validate it on the server, enroll the new user with the referrer value, read the relationship back, and then check configured qualification and reward state. The referral value never chooses the identity of the authenticated widget.
Referrer and referee are different users
In these examples, CUSTOMER_0900 is the referrer and CUSTOMER_1001 is the new referee. The new user keeps its own stable identity in externalId; the referrer's validated value is sent separately in the top-level referral property. First-touch persistence is an example broker policy, not a universal platform promise.
Referral attribution to authenticated display
Attribution input, relationship state, qualification, rewards, and widget identity are separate.
Capture the incoming value
BrowserRead the configured query parameter before redirects or registration, and carry it as onboarding input. It is not a widget identity.
- Broker server
Resolve the value against the configured referral mapping, then create CUSTOMER_1001 with CUSTOMER_0900 in referral only when policy validation passes.
Read the relationship
Broker serverRead the new user by email or returned platform ID, then read the referrer summary with the same identifier namespace used by the program.
Check qualification and rewards
Returning.AI configurationSend the real configured business state through its approved field, event, or workflow path. Relationship presence is not stage completion or a reward balance update.
- Portal server
Mint the widget token from the trusted CUSTOMER_1001 session and mount the supplied referral bundle. Never sign the query value as the logged-in user.
Discover the configured programs
Use GET https://api.returning.ai/v1/referral/programs?lang=en with the server-side community API key and getUserData permission. This is a read-only configuration call. Its program IDs, stage names, condition keys, and reward amounts belong to the community; do not hard-code the placeholders below.
export RAI_API_BASE="https://api.returning.ai/v1"
export COMMUNITY_API_KEY="<COMMUNITY_API_KEY>" # server-side only
curl --fail-with-body --request GET \
"$RAI_API_BASE/referral/programs?lang=en" \
--header "Authorization: Bearer $COMMUNITY_API_KEY" \
--header "Accept: application/json"A valid response can contain an empty data array when no program is enabled. Save the configured program and stage details for launch checks rather than treating discovery as proof that a member has qualified.
{
"meta": {
"status": "success",
"statusCode": 200
},
"data": [
{
"enabled": true,
"programId": "<PROGRAM_ID>",
"name": "<CONFIGURED_PROGRAM_NAME>",
"stages": [
{
"stageId": "<STAGE_ID>",
"stageNumber": 1,
"name": "<CONFIGURED_STAGE_NAME>",
"mainConditions": [],
"rewards": {
"xp": 0,
"coins": 0
}
}
]
}
]
}Capture before registration
Capture the query value before a redirect or registration form discards it. The parameter name is an integration setting, so replace <REFERRAL_QUERY_PARAM> with the configured name. The browser can carry this value to your server, but the server must validate the allowed namespace, current program policy, and referrer mapping before placing it in a create request.
// Browser-only capture. The parameter name is configured by your integration.
const referralValue = new URLSearchParams(window.location.search)
.get("<REFERRAL_QUERY_PARAM>");
// Keep this as onboarding input. Do not use it as the signed-in widget identity.
if (referralValue && !sessionStorage.getItem("pendingReferral")) {
sessionStorage.setItem("pendingReferral", referralValue);
}This example keeps the first value in this tab and origin only. Clear it when signup completes or is cancelled. If onboarding crosses origins, tabs, or sessions, carry the validated attribution in your server-side onboarding record under the agreed retention policy.
The query value is not authentication
Never use the referral query value to decide which user receives a signed widget token. Resolve the host session independently, then sign the registered user's configured identifier on the server.
Create the referee with validated attribution
After server validation, create the new community user through the public /v1/users route. The community key needs createUser. The request below uses the active identifier example; use the configured custom-field form instead when that is the community's registration mapping.
export RAI_API_BASE="https://api.returning.ai/v1"
export COMMUNITY_API_KEY="<COMMUNITY_API_KEY>" # server-side only
curl --fail-with-body --request POST "$RAI_API_BASE/users" \
--header "Authorization: Bearer $COMMUNITY_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"firstname": "Referee",
"lastname": "Example",
"username": "customer_1001",
"email": "customer-1001@example.com",
"accessLevel": 1,
"joinServer": true,
"sendEmail": false,
"emailPassword": false,
"externalId": "CUSTOMER_1001",
"referral": "CUSTOMER_0900"
}'Read the create result and then read the new user back by its known email or returned platform ID. If the create request times out or returns a 5xx, use bounded readback before any resubmission. If the user was created but referral association is delayed or missing, keep the user and reconcile the relationship; never create the referee again.
Read the referrer summary
Use POST https://api.returning.ai/v1/referral/summary with the community key and getUserData. The identifier type must match the configured namespace. The supported types are id, email, data-email, data-customer-id, cognito-sub, and custom-field. For custom-field, send the exact configured fieldName; do not guess a field or treat a custom-field lookup on a different endpoint as proof that it works here.
export RAI_API_BASE="https://api.returning.ai/v1"
export COMMUNITY_API_KEY="<COMMUNITY_API_KEY>" # server-side only
curl --fail-with-body --request POST "$RAI_API_BASE/referral/summary" \
--header "Authorization: Bearer $COMMUNITY_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"identifier": {
"type": "data-customer-id",
"value": "CUSTOMER_0900"
},
"pagination": {
"limit": 20
}
}'The custom-field variant is a request body change, not a second identity value. Use it only when the community owner confirms the field mapping.
{
"identifier": {
"type": "custom-field",
"fieldName": "customer_id",
"value": "CUSTOMER_0900"
},
"pagination": {
"limit": 20
}
}Set pagination.limit between 1 and 100. If the response returns anextCursor, send that exact opaque value in the next request'spagination.cursor. Do not decode it, manufacture it, or reuse it with another user, program, community, or key.
{
"data": {
"user": {
"userId": "<REFERRER_PLATFORM_USER_ID>",
"email": "customer-0900@example.com"
},
"invite": {
"url": "https://portal.example.com/join?ref=CUSTOMER_0900"
},
"activity": {
"totalReferrals": 1,
"inProgress": 1,
"completed": 0
},
"referees": [
{
"userId": "<REFEREE_PLATFORM_USER_ID>",
"username": "customer_1001",
"progressByProgram": []
}
],
"pagination": {
"hasNextPage": false,
"nextCursor": null
}
}
}When the response includes data.invite.url, render that returned URL. Do not manufacture a platform URL from an invite code or substitute platformUrl when the configured invite.url is present. A valid member with no referrals can return 200, zero counters, and an empty referees array.
Keep relationship, qualification, and reward separate
| State | Evidence | Do not infer |
|---|---|---|
| Attribution input | The browser captured a configured query value and the server accepted it after policy validation. | That the value identifies the signed-in widget user or is a valid referrer without mapping checks. |
| Relationship | The referrer summary contains the new referee or the configured relationship readback shows the pair. | That a stage condition has completed or a reward has been credited. |
| Qualification | The configured field, event, or workflow state has been saved and its evaluation readback is complete. | That every field write maps to a milestone or that evaluation is immediate. |
| Reward | The configured reward or ledger readback confirms the award. The Returning.AI owner supplies that readback operation. | That a completed stage or its configured reward amount proves a balance was credited, or guarantees retroactive credit or timing. |
For a field-backed condition, follow the field lifecycle and read the saved value before polling referral progress. Do not replay a field write just because one summary read is still zero.
Retry reads, reconcile writes
Programs and referral summaries are read-only surfaces. A bounded retry can help with a transient 5xx or upstream failure, while 400 responses call for correcting the identifier, field name, or pagination and 401/403 responses call for fixing the server key or its permission. A 404 user result calls for checking the environment, namespace, and exact value.
Do not replay enrollment for referral delay
User creation, referral association, field writes, qualification, and reward aggregation are separate state changes. A successful create plus a missing summary row is partial success. Read the created user, confirm the configured referrer mapping, poll the read-only summary within an integration-owned window, and send unresolved cases to the owner. Do not create another user.
Mount the current referral widget
The referral widget uses the same custom-widget and Access Key Embed flow as other configured experiences. The token request is server-only and signs the referee's own configured identity, here CUSTOMER_1001. The browser receives the short-lived embed token, never the server credentials. Signed token contents are browser-visible; do not put secrets or unnecessary personal data inside them.
# Server-side example. Replace placeholders or use the authenticated starter.
curl --fail-with-body --request POST \
"https://api-v2.returning.ai/v2/api/widget-access-keys/token" \
--header "Content-Type: application/json" \
--data '{
"accessId": "<RAI_ACCESS_ID>",
"accessKey": "<RAI_ACCESS_KEY>",
"userIdentifiers": {
"data-customer-id": "CUSTOMER_1001"
}
}'<script src="https://unpkg.com/@returningai/widget-sdk@1.8.11/dist/rai-widget.iife.js"></script>
<rai-custom-widget
widget-id="<WIDGET_ID>"
bundle-url="<SUPPLIED_BUNDLE_URL>"
embed-token="<SHORT_LIVED_JWT>"
height="auto"
></rai-custom-widget>Keep the supplied widget-id and bundle-url pair together. On token expiry, fetch a fresh token from the server and update embed-token on the existing element. Removing the element on logout is browser cleanup; it is not proof that an already issued token was revoked.
Continue to launch checks
Open the launch checklist for the environment mapping, relationship and qualification cases, named owners, and pause/replay rules. If enrollment itself is not settled, return to Enroll traders before testing referral progress.
Operation references
Use the current read-only operation pages for referral programs and referral summary. For enrollment details, see create a user. These pages document the operation shapes; program configuration and identity mapping still belong to the community setup owner.