# Custom fields and milestones

A field definition, a saved user value, and a configured milestone evaluation are separate states. Define the field first, write a type-matched value for a resolved user, read the saved history and current value back, and only then check the configured milestone or widget projection.

> A saved value is not a reward result
>
> A successful field write proves that a value was accepted and stored. It does not prove that a mission, referral stage, reward, balance, or widget projection has finished. Use the configured evaluation path and its readback owner for that next checkpoint.

Field value to widget state

Each step has a different owner and readback surface. Do not collapse them into one write.

1. Define the field

   Community owner

   Create a stable field key and type with a community API key that has userFields permission.
2. [Resolve the user](https://docs.returning.ai/broker-integrations/users.md#readback)

   Broker server

   Use the platform user ID or another identifier accepted by the current user-first field route. CUSTOMER_1001 is not automatically a valid path ID.
3. Save and read the value

   Broker server

   Write a typed value with the user-first history route, then read the narrow history/current projection before moving on.
4. Evaluate configured milestone

   Returning.AI configuration

   A configured workflow, mission, or referral stage evaluates the saved state separately. Timing and reward completion are not promised by the field response.
5. [Refresh the widget](https://docs.returning.ai/widget-sdk.md)

   Portal server

   After the authoritative state is ready, use the normal authenticated widget flow. A field write does not require a guessed milestone endpoint.

## Follow the field lifecycle

| State | What it means | Readback question |
| --- | --- | --- |
| Definition and type | The community has a stable key such as `account_approved` and a declared type. | Does the configured field exist with the type your payload will send? |
| Saved value and history | A user-first history write accepted a typed value for one resolved user. | Does the narrow history read show the expected value and action? |
| Configured milestone | A separate workflow, mission, or referral condition evaluates the saved state. | Did the configured evaluator report the expected stage or qualification? |
| Widget projection | The authenticated widget reads the current user state after the backend path completes. | Does a fresh or refreshed widget view show the authoritative result? |

## Define a stable field

Use `POST https://api.returning.ai/v1/communities/<COMMUNITY_OBJECT_ID>/user-fields` with the server-side community API key and `userFields` permission. The display name is for people; `data.field` is the stable technical key used by later writes and reads. Confirm owner/admin access and the target type before creating the field.

`define-field.sh`

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

curl --fail-with-body --request POST \
  "$RAI_API_BASE/communities/$COMMUNITY_OBJECT_ID/user-fields" \
  --header "Authorization: Bearer $COMMUNITY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Account Approved",
    "field": "account_approved",
    "type": "boolean"
  }'
```

The example intentionally leaves out `defaultValue`. Add a default only when the target field-definition contract confirms its type and behavior. Save the returned field object ID if your setup uses it, but the example below uses the stable field key.

`field-definition-response.json`

```json
{
  "data": {
    "_id": "<FIELD_OBJECT_ID>",
    "name": "Account Approved",
    "field": "account_approved",
    "type": "boolean"
  }
}
```

## Write a value for a resolved user

Resolve the trader from enrollment first. Use the returned `<PLATFORM_USER_ID>` in the user-first route below. `CUSTOMER_1001` is the broker identity and is not automatically accepted as `<userIdentifier>` just because it was accepted as an active external ID or widget identifier.

The current public-shaped route is `POST /v1/communities/<community>/users/<user>/user-fields/<field>/histories`. It uses the same community API key family and the `userFields` permission, not SDK Access Key credentials.

`write-account-approved.sh`

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

curl --fail-with-body --request POST \
  "$RAI_API_BASE/communities/$COMMUNITY_OBJECT_ID/users/$PLATFORM_USER_ID/user-fields/account_approved/histories" \
  --header "Authorization: Bearer $COMMUNITY_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "value": true,
    "action": "overwrite"
  }'
```

The response excerpt below is the readback handle to inspect, not a promise of a full response shape. Confirm the status, field name, typed value, and action before checking any configured milestone.

`field-write-response.json`

```json
{
  "meta": {
    "statusCode": 201
  },
  "data": {
    "_id": "<HISTORY_OBJECT_ID>",
    "createdAt": "2026-09-07T09:00:00.000Z",
    "fieldName": "account_approved",
    "value": true,
    "action": "overwrite"
  }
}
```

### Keep values and actions type-matched

The current history body accepts strings, numbers, booleans, and supported arrays. The action defaults to `overwrite`. Use `overwrite` for the boolean state shown here; use `increase` or `decrease` only for a numerical field when the configured business rule calls for an additive change. A numeric zero must remain JSON number `0`, not the string `"0"`.

`numerical-overwrite-body.json`

```json
{
  "value": 0,
  "action": "overwrite"
}
```

Do not copy a boolean payload into a numerical field, treat a false value as missing, or invent a select-value write example. Confirm the configured field type and accepted values before using any type that is not shown in the current operation contract.

## Read the saved history

Read the same user and field after the write. Keep the request user-first and prefer the platform ID or another identity the route explicitly accepts. The current custom-field lookup path is not a universal substitute for this narrow history read.

`read-field-history.sh`

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

curl --fail-with-body --request GET \
  "$RAI_API_BASE/communities/$COMMUNITY_OBJECT_ID/users/$PLATFORM_USER_ID/user-fields/account_approved/histories?page=1&limit=5" \
  --header "Authorization: Bearer $COMMUNITY_API_KEY" \
  --header "Accept: application/json"
```

This request reads the newest five history rows, not the current field value. Match the returned write receipt's row ID and`createdAt`, user, field, typed value, and action; matching an old`true/overwrite` row does not identify this write. If a newer write exists, account for it before judging the expected current value. The excerpt below is synthetic.

`field-history-response.json`

```json
{
  "data": [
    {
      "_id": "<HISTORY_OBJECT_ID>",
      "createdAt": "2026-09-07T09:00:00.000Z",
      "fieldName": "account_approved",
      "value": true,
      "action": "overwrite"
    }
  ]
}
```

## Read the current value separately

With the resolved platform ID, request the exact field key through`POST /v1/users/info`. This read requires`getUserData`, separate from the `userFields`permission on history operations. Replace `account_approved`with your configured key when using a different field.

`read-current-field.sh`

```bash
export RAI_API_BASE="https://api.returning.ai/v1"
export COMMUNITY_API_KEY="<COMMUNITY_API_KEY>" # requires getUserData

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":["account_approved"]}'
```

`current-field-response-excerpt.json`

```json
{
  "data": {
    "userId": "<PLATFORM_USER_ID>",
    "customFields": { "account_approved": true }
  }
}
```

Check `data.userId` and the typed current value in`data.customFields.account_approved`, then compare with the latest confirmed business state. History proves a recorded mutation; this selected-field read checks the current projection. Neither proves milestone evaluation or reward credit.

> Unresolved projection or history
>
> Field visibility and projection behavior vary by target. A missing, stale, or unexpectedly formatted value, including a string`"null"`, is unresolved. If bounded reads cannot verify the current state or identify the mutation, ask the Returning.AI data owner for the authoritative current-value and history check for this user and field. Record that confirmation before proceeding. Missing readback is never permission to replay an additive write.

## Check the configured milestone separately

A field can be used by a configured onboarding, KYC, deposit, or referral condition, but the field definition does not create that condition. If the community has configured`account_approved` as a boolean milestone input, write the real broker state and then wait for the configured evaluator. Do not write a fake value to force a reward.

1. Confirm the definition and type for the configured field.
2. Write the real business state with the resolved user identity.
3. Identify the mutation in newest-first history, then separately read and record the current value.
4. Check the configured workflow, mission, or referral read surface for evaluation.
5. Only after the authoritative state is ready, verify the authenticated widget projection.

The [milestone read reference](https://dev.returning.ai/users/get-milestones) exists, but an endpoint listing is not a successful acceptance example for your quest. Ask the Returning.AI setup owner for the existing configured IDs, the supported request for that user and a recorded successful result. The public reference marks the successful progress envelope as unproven without those inputs; do not invent a completion field or infer completion from a field write.

For an already-open widget, verify the same signed-in user and saved state first, then use the supplied bundle's confirmed refresh action and record the visible result. Renewing its token does not prove data refreshed. If the display action or signal is unconfirmed, keep widget acceptance open in the [handover sheet](https://docs.returning.ai/broker-integrations/launch-checklist.md#decision-gates); do not resend a write or award to refresh the screen.

If `account_approved` later changes back to `false`, your rewards owner and Returning.AI setup owner must agree whether completion stays earned, reopens or changes rewards. Test that configured outcome before enabling the rule. This guide does not prescribe reversal or clawback behavior.

> No direct milestone write
>
> This flow does not call a guessed `/users/milestones/` endpoint. A milestone is a configured evaluation reached through the approved user, field, event, or workflow path. The platform does not publish a universal completion time for that evaluation or the resulting widget refresh.

## Recover safely from uncertain writes

A timeout, 5xx response, or partial side effect does not make a repeated field mutation safe. Read the narrow history and current projection first. For `increase` or `decrease`, never blindly add the same change again. Repeated `overwrite` calls can still create history or downstream evaluation side effects, so an ambiguous result goes to the integration owner after bounded readback rather than an automatic replay.

- Keep the original user, field, value, and action together when investigating an uncertain result.
- Do not add a retry header that the current public operation does not document.
- If the value is saved but the milestone is not visible, reconcile evaluation separately; do not rewrite the value just to poll.
- Keep successfully saved state and failed downstream qualification as separate records for launch review.

## Continue to launch checks

[Open the launch checklist](https://docs.returning.ai/broker-integrations/launch-checklist.md) for owners, evidence, pause/replay rules, and the cases where a saved value still needs a separate configured readback. For the widget-side identity and token contract, use the [custom widget quickstart](https://docs.returning.ai/widget-sdk.md).

## Operation references

Use the current operation pages for the full route details: [create a field definition](https://dev.returning.ai/user-fields/create), [update a user field value](https://dev.returning.ai/user-fields/update-user-field-value), and [list user field history](https://dev.returning.ai/user-fields/list-user-field-history), and [read selected current user fields](https://dev.returning.ai/users/lookup-user). The links describe the operation; your community owner still confirms the configured field and identity mapping.
