# Enroll traders

Enroll a trader after the broker has committed the registration. Keep a stable, broker-owned person key separate from trading account IDs, then read the Returning.AI user and membership back before you mint a widget token.

> Two credential families
>
> Enrollment uses a server-side community API key with the separate `createUser`, `getUserData`, and, when fields are written, `userFields` permissions. Widget Access Key credentials are a different server-only pair used later for token exchange. Neither credential family belongs in browser code.

Enrollment boundary before the widget

The broker owns the registration commit. Returning.AI owns the community user and membership readback.

1. Commit broker registration

   Broker server

   Persist the customer record and its stable broker key first. Keep ACCOUNT_2001 and other trading accounts as account records, not new loyalty users by accident.
2. Create the community user

   Broker server

   Call POST /v1/users with the community key, the configured identity, and joinServer: true. The public gateway is api.returning.ai/v1.
3. Read identity and membership back

   Broker server

   Save a successful joinServer: true create receipt. Read the user with the configured field allowlist. Existing or ambiguous users need a separate confirmed membership/activation readback.
4. [Open your first widget](https://docs.returning.ai/widget-sdk.md)

   Portal server

   Only after the readback is consistent, mint an Access Key Embed token for the same configured identifier and mount the supplied widget pair.

## Choose a stable person key

Use a durable broker customer identity such as `CUSTOMER_1001` for the person-level mapping. Keep `ACCOUNT_2001` and `ACCOUNT_2002` attached to that person in broker data. Trading account IDs are not a reason to create separate Returning.AI users unless your approved business mapping says they are.

| Value | Owner and meaning | Use in this flow |
| --- | --- | --- |
| `CUSTOMER_1001` | Broker stable person identifier | Top-level `externalId`, or the configured custom field fallback. Use the same namespace for the widget only when the widget is configured for it. |
| `ACCOUNT_2001` | Broker trading account identifier | Keep it in broker account and trading records. Do not use it as the user identity by default. |
| `<PLATFORM_USER_ID>` | Returning.AI numeric platform ID returned by create | Save it as an opaque string. It is separate from `CUSTOMER_1001` and is a reliable readback selector after create. |
| `customer-1001@example.com` | Known email used by the current lookup operation | A lookup input, not a guaranteed response field. Matching an email alone is not proof of the configured broker identity or usable membership. |

## Enroll after the broker commit

The browser should not create a community user from a query parameter or an unverified page event. Your server should receive the broker registration, validate the stable key and email, then make the community request. This keeps retries tied to a broker record that can be reconciled by an owner.

1. Commit the broker customer and account mapping.
2. Confirm which community field is the active identifier, or choose the configured custom-field fallback.
3. Submit the user request with `joinServer: true` so the create path can return a usable membership result.
4. Save the returned platform and membership identifiers, then perform the readback below.

## Create the user

Use `POST https://api.returning.ai/v1/users` with a Bearer community API key that has`createUser`. The key selects the community, so do not add `communityId` to this request body. The example uses the active identifier form; confirm that mapping with the community owner before sending it.

`enroll-trader.sh`

```bash
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": "Ada",
    "lastname": "Lovelace",
    "username": "customer_1001",
    "displayname": "Customer 1001",
    "email": "customer-1001@example.com",
    "accessLevel": 1,
    "joinServer": true,
    "sendEmail": false,
    "emailPassword": false,
    "externalId": "CUSTOMER_1001"
  }'
```

If the community does not have an active external identifier, replace `externalId` with the configured stable field in `customFields`. The endpoint accepts the current spelling `fieldIdorName`; use one identity path, not two competing values.

`custom-field-body.json`

```json
{
  "firstname": "Ada",
  "lastname": "Lovelace",
  "username": "customer_1001",
  "displayname": "Customer 1001",
  "email": "customer-1001@example.com",
  "accessLevel": 1,
  "joinServer": true,
  "sendEmail": false,
  "emailPassword": false,
  "customFields": [
    {
      "fieldIdorName": "customer_id",
      "value": "CUSTOMER_1001"
    }
  ]
}
```

### Save the create result

The following is a response-field excerpt, not a full schema. Treat `data.userId` as an opaque platform identifier even when it is numeric. Treat `communityMemberId` as a membership checkpoint, not as the broker customer key.

`create-response.json`

```json
{
  "status": "success",
  "data": {
    "userId": "<PLATFORM_USER_ID>",
    "communityMemberId": "<COMMUNITY_MEMBER_OBJECT_ID>",
    "externalId": "CUSTOMER_1001",
    "created": true
  }
}
```

## Read identity and membership back

Read the user after create, before field writes or widget token minting. The current tested selectors are a known email or the numeric platform ID returned by create. Custom-field lookup and projection are environment-sensitive, so do not make an unverified broker-key lookup your only recovery path. Explicitly request the configured identity field in`customFields`; without that allowlist the response can contain an empty field map. The example assumes the configured key is`customer_id`. Replace it with your actual field key.

`read-user.sh`

```bash
export RAI_API_BASE="https://api.returning.ai/v1"
export COMMUNITY_API_KEY="<COMMUNITY_API_KEY>" # server-side only

# Use the known email when it is available.
curl --fail-with-body --request POST "$RAI_API_BASE/users/info" \
  --header "Authorization: Bearer $COMMUNITY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"idOrEmail":"customer-1001@example.com","customFields":["customer_id"]}'

# Or use the opaque platform ID returned by create.
curl --fail-with-body --request POST "$RAI_API_BASE/users/info" \
  --header "Authorization: Bearer $COMMUNITY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"idOrEmail":"<PLATFORM_USER_ID>","customFields":["customer_id"]}'
```

Compare `data.userId` with the saved platform ID and`data.customFields.customer_id` with `CUSTOMER_1001`. Email selects the lookup; it is not a returned-field assertion. If the target cannot expose the configured mapping, keep enrollment unresolved and ask the Returning.AI owner to verify the broker-to-platform mapping. An empty field map or a same-email match does not satisfy that check.

`read-user-response.json`

```json
{
  "data": {
    "_id": "<MONGO_USER_OBJECT_ID>",
    "userId": "<PLATFORM_USER_ID>",
    "username": "customer_1001",
    "customFields": {
      "customer_id": "CUSTOMER_1001"
    }
  }
}
```

### Confirm usable membership

For a new user, retain the confirmed successful create receipt for your`joinServer: true` request, including its membership identifier, together with the matching identity readback. A membership row alone is insufficient: invitation-pending rows can exist, and ordinary user lookup does not return the activation state.

For an existing user or an ambiguous create without that receipt, require the target's supported membership/activation readback or an explicit Returning.AI-owned confirmation of usable membership in the target community. Record the platform user ID, community, confirmed state, and evidence reference. Recover the missing receipt through this check, never another create request. Do not mint a widget token while it is unresolved.

A successful user lookup is a profile/identity read, not an activation check. Do not treat HTTP 200, a membership identifier or the word “active” in a lookup description as permission to mint. For unattended recovery, obtain the supported state check for your actual environment from the Returning.AI community owner; otherwise use the explicit confirmation above and keep that user blocked until it is recorded. Name the contact and evidence in the [handover decision gates](https://docs.returning.ai/broker-integrations/launch-checklist.md#decision-gates).

## Handle existing and ambiguous outcomes

An existing user is a reconciliation case. Read it by known email or saved platform ID, compare the configured identity, and continue only after both the mapping and the [usable membership check](#membership-check) are confirmed. Do not merge unrelated users because an email happens to match, and do not generate a second broker key to avoid a conflict.

> A failed response can hide a committed user
>
> A timeout, connection failure, or 5xx response does not prove that no user or membership was saved. Do not immediately retry `POST /v1/users`. Perform bounded read-only checks with the known email or returned platform ID. If the outcome stays unresolved after the integration's read budget, stop automatic creation and send the case to the owner for reconciliation before resubmitting.

- A 404 or empty read during an ambiguous create is not proof that the original write did not finish.
- A duplicate result means read and reconcile the existing identity; it is not permission to create another user.
- If referral association or a later milestone read is delayed, keep the committed user and reconcile that follow-up separately. Never recreate the user for a referral failure.
- Keep the bounded retry policy endpoint-specific. This create example does not add a retry header that the current public operation does not document or promise an all-or-nothing result.

## Open your first widget

Once the platform ID, confirmed usable membership, and configured identity readback agree, continue with the server-side Access Key flow. The community API key used here is not the SDK`accessId`/`accessKey` pair. Mint the token from the trusted portal session and sign the same configured identity value used during enrollment.

[Open your first widget](https://docs.returning.ai/widget-sdk.md) with the current custom-widget quickstart, including token refresh and sign-out cleanup. Then [choose a data path](https://docs.returning.ai/broker-integrations.md#options) for lifecycle events and trading facts.

## Operation references

Use the operation-specific API pages for the full current request contract: [create a user](https://dev.returning.ai/users/create-user) and [get user data](https://dev.returning.ai/users/lookup-user). Confirm the active identifier mapping and permissions for the target community before moving from these synthetic examples to your server.
