> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phosra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Reconcile family drift

> A connected platform reports children Phosra has never seen. Pull both rosters, diff them, and add the missing kids so the family in Phosra matches the family on the platform — the full detect-diff-reconcile loop, run live against the open sandbox.

Families change on the platform side without telling Phosra. A parent adds a third child to their
console, a sibling ages in, a profile is renamed — and now the family Phosra knows about no longer
matches the family the platform reports. This recipe pulls **both rosters** — Phosra's children and
the platform's profiles — diffs them, and reconciles by adding the children Phosra is missing, so
policies and enforcement cover every child the platform actually has.

```mermaid theme={null}
flowchart LR
  A["GET /families/{id}/children<br/>Phosra roster"] --> C["diff by name"]
  B["GET /oauth/profiles<br/>platform roster"] --> C
  C --> D{"missing in<br/>Phosra?"}
  D -->|yes| E["POST /families/{id}/children<br/>add each"]
  D -->|no| F["in sync"]
  E --> G["GET /families/{id}/children<br/>confirm parity"]
```

Every request and response below is **verbatim live output**, captured against
`https://phosra-api-sandbox-production.up.railway.app`. No API key, nothing to install — the
platform roster comes from the sandbox reference provider (`did:ocss:loopline`).

<Info>
  **Sandbox-first.** These calls run against the open sandbox with no credential. In production the
  platform roster comes from the real provider you connected (via the OAuth ceremony in
  [Connect a platform](/guides/connect-a-platform)) and the family endpoints are family-scoped — the
  diff-and-reconcile logic is identical.
</Info>

## Before you start

This recipe uses a family with **one** child in Phosra (Mia) connected to a platform that reports
**three** (Mia, Leo, Ava). Set the base URL and family once:

```bash theme={null}
export PHOSRA_BASE="https://phosra-api-sandbox-production.up.railway.app"
export FAMILY_ID="7805eb97-c3c6-4836-8c66-69c77b81fd2e"
```

<Steps>
  <Step title="Pull the Phosra roster">
    [`GET /families/{familyID}/children`](/api-reference/children/get-families-children) returns the
    children Phosra currently knows about for this family.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s "$PHOSRA_BASE/api/v1/families/$FAMILY_ID/children"
      ```

      ```typescript TypeScript theme={null}
      const BASE = "https://phosra-api-sandbox-production.up.railway.app";
      const familyId = "7805eb97-c3c6-4836-8c66-69c77b81fd2e";

      const phosraKids = await (
        await fetch(`${BASE}/api/v1/families/${familyId}/children`)
      ).json();
      const phosraNames = new Set(phosraKids.map((c: any) => c.name));
      ```

      ```python Python theme={null}
      import requests

      BASE = "https://phosra-api-sandbox-production.up.railway.app"
      family_id = "7805eb97-c3c6-4836-8c66-69c77b81fd2e"

      phosra_kids = requests.get(f"{BASE}/api/v1/families/{family_id}/children", timeout=30).json()
      phosra_names = {c["name"] for c in phosra_kids}
      ```
    </CodeGroup>

    Live `200` — Phosra has **one** child:

    ```json theme={null}
    [
      {
        "id": "2b26d5ff-22d8-44b0-9992-c6c9f1df1676",
        "family_id": "7805eb97-c3c6-4836-8c66-69c77b81fd2e",
        "name": "Mia",
        "birth_date": "2013-01-15T00:00:00Z"
      }
    ]
    ```
  </Step>

  <Step title="Pull the platform roster">
    The connected platform reports its own view of the family through
    [`GET /oauth/profiles`](/guides/connect-a-platform) — the child list you receive after the connect
    ceremony, authorized with the `access_token` that ceremony returned. The `authorize` step is
    **browser-driven** (the parent grants consent), so the TypeScript and Python tabs start from the
    `accessToken` you already hold; the cURL tab runs the whole chain inline as a sandbox shortcut.

    <CodeGroup>
      ```bash cURL theme={null}
      # Sandbox shortcut: approve inline → code → token → profiles.
      # (In production the parent's browser does the authorize step; you receive the code
      #  at your redirect_uri — see Connect a platform.)
      LOC=$(curl -s -o /dev/null -D - \
        "$PHOSRA_BASE/oauth/authorize?client_id=did:ocss:loopline&decision=approve&redirect_uri=https://loopline.example/callback&state=drift" \
        | grep -i '^location:' | tr -d '\r')
      CODE=$(echo "$LOC" | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p')

      AT=$(curl -s -X POST "$PHOSRA_BASE/oauth/token" -H "Content-Type: application/json" \
        -d "{\"grant_type\":\"authorization_code\",\"code\":\"$CODE\",\"client_id\":\"did:ocss:loopline\",\"redirect_uri\":\"https://loopline.example/callback\"}" \
        | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")

      curl -s "$PHOSRA_BASE/oauth/profiles" -H "Authorization: Bearer $AT"
      ```

      ```typescript TypeScript theme={null}
      // accessToken is the token from your completed connect ceremony
      // (see Connect a platform → the token step for how to obtain it).
      const accessToken = "sbxtok_jVSg5vetI0NnX9PMZ-XOVf1hr8sHoBOV";

      const profiles = await (
        await fetch(`${BASE}/oauth/profiles`, {
          headers: { Authorization: `Bearer ${accessToken}` },
        })
      ).json();
      ```

      ```python Python theme={null}
      # access_token is the token from your completed connect ceremony
      # (see Connect a platform → the token step for how to obtain it).
      access_token = "sbxtok_jVSg5vetI0NnX9PMZ-XOVf1hr8sHoBOV"

      profiles = requests.get(
          f"{BASE}/oauth/profiles",
          headers={"Authorization": f"Bearer {access_token}"},
          timeout=30,
      ).json()
      ```
    </CodeGroup>

    Live `200` — the platform reports **three** children:

    ```json theme={null}
    [
      { "id": "mia", "displayName": "Mia", "subject_ref": "a11ce0fa-0000-4000-8000-0000000000a1", "kind": "child" },
      { "id": "leo", "displayName": "Leo", "subject_ref": "a11ce0fa-0000-4000-8000-0000000000a2", "kind": "child" },
      { "id": "ava", "displayName": "Ava", "subject_ref": "a11ce0fa-0000-4000-8000-0000000000a3", "kind": "child" }
    ]
    ```

    Phosra has Mia; the platform has Mia, **Leo, and Ava**. Leo and Ava are the drift.
  </Step>

  <Step title="Diff the rosters">
    Compute which platform children Phosra is missing. Match on the child's name (`displayName` on the
    platform side, `name` on Phosra's) — a platform's `subject_ref` is its own identifier and will not
    equal a Phosra child `id`, so it is not the join key.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      const missing = profiles.filter(
        (p: any) => p.kind === "child" && !phosraNames.has(p.displayName),
      );
      // missing → [{ displayName: "Leo", … }, { displayName: "Ava", … }]
      ```

      ```python Python theme={null}
      missing = [p for p in profiles
                 if p["kind"] == "child" and p["displayName"] not in phosra_names]
      # missing → [Leo, Ava]
      ```

      ```bash cURL theme={null}
      # With the two JSON blobs saved to phosra.json and profiles.json:
      python3 - <<'PY'
      import json
      have = {c["name"] for c in json.load(open("phosra.json"))}
      missing = [p["displayName"] for p in json.load(open("profiles.json"))
                 if p["kind"] == "child" and p["displayName"] not in have]
      print(missing)   # ['Leo', 'Ava']
      PY
      ```
    </CodeGroup>

    <Warning>
      **Diff both directions.** This recipe adds children the platform has but Phosra lacks. The reverse —
      a child in Phosra the platform no longer reports — is a *removal* signal, but never auto-delete: a
      missing profile can mean the parent temporarily hid it. Surface reverse drift for review rather
      than deleting policies and enforcement history.
    </Warning>
  </Step>

  <Step title="Add the missing children">
    For each missing child, [`POST /families/{familyID}/children`](/api-reference/children/add-child)
    adds them to the Phosra family. Only a name and birth date are required.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST "$PHOSRA_BASE/api/v1/families/$FAMILY_ID/children" \
        -H "Content-Type: application/json" \
        -d '{ "name": "Leo", "birth_date": "2016-09-03" }'

      curl -s -X POST "$PHOSRA_BASE/api/v1/families/$FAMILY_ID/children" \
        -H "Content-Type: application/json" \
        -d '{ "name": "Ava", "birth_date": "2010-02-20" }'
      ```

      ```typescript TypeScript theme={null}
      for (const child of missing) {
        await fetch(`${BASE}/api/v1/families/${familyId}/children`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            name: child.displayName,
            birth_date: birthDateFor(child), // from your own records / an age hint
          }),
        });
      }
      ```

      ```python Python theme={null}
      for child in missing:
          requests.post(
              f"{BASE}/api/v1/families/{family_id}/children",
              json={"name": child["displayName"], "birth_date": birth_date_for(child)},
              timeout=30,
          )
      ```
    </CodeGroup>

    Live `201` for the Ava add — a new Phosra child `id`:

    ```json theme={null}
    {
      "id": "66c24a80-b7ae-4929-88b1-71dd16aea7b7",
      "family_id": "7805eb97-c3c6-4836-8c66-69c77b81fd2e",
      "name": "Ava",
      "birth_date": "2010-02-20T00:00:00Z",
      "created_at": "2026-07-06T11:30:43.321043959Z"
    }
    ```

    <Note>
      The platform profile does not carry a birth date, so supply one from your own records or an age
      band. A newly-added child has **no policy** yet — generate an age-appropriate one next (see
      [Onboard a multi-child household](/guides/recipes/multi-child-household), which turns a birth date
      into a full rule set in one call).
    </Note>
  </Step>

  <Step title="Confirm parity">
    Re-pull the Phosra roster. It now matches the platform — every child the console reports exists in
    Phosra:

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s "$PHOSRA_BASE/api/v1/families/$FAMILY_ID/children"
      ```

      ```typescript TypeScript theme={null}
      const after = await (
        await fetch(`${BASE}/api/v1/families/${familyId}/children`)
      ).json();
      // after.map(c => c.name) → ["Mia", "Leo", "Ava"]
      ```

      ```python Python theme={null}
      after = requests.get(f"{BASE}/api/v1/families/{family_id}/children", timeout=30).json()
      # [c["name"] for c in after] → ["Mia", "Leo", "Ava"]
      ```
    </CodeGroup>

    Live `200` — three children, matching the platform's three profiles:

    ```json theme={null}
    [
      { "id": "2b26d5ff-22d8-44b0-9992-c6c9f1df1676", "name": "Mia" },
      { "id": "567b2b02-e576-413c-a679-32af2db346dd", "name": "Leo" },
      { "id": "66c24a80-b7ae-4929-88b1-71dd16aea7b7", "name": "Ava" }
    ]
    ```

    The drift is reconciled. Give Leo and Ava a policy, then enforce so the new children are actually
    covered on every connected platform.
  </Step>
</Steps>

## The whole flow at a glance

| # | Step            | Call                              | Live result                      |
| - | --------------- | --------------------------------- | -------------------------------- |
| 1 | Phosra roster   | `GET /families/{id}/children`     | 1 child — Mia                    |
| 2 | Platform roster | `GET /oauth/profiles`             | 3 profiles — Mia, Leo, Ava       |
| 3 | Diff by name    | (local)                           | missing: Leo, Ava                |
| 4 | Add missing     | `POST /families/{id}/children` ×2 | Leo `567b2b02…`, Ava `66c24a80…` |
| 5 | Confirm         | `GET /families/{id}/children`     | 3 children — in sync             |

## Next steps

<CardGroup cols={2}>
  <Card title="Onboard a multi-child household" icon="users" href="/guides/recipes/multi-child-household">
    Turn each newly-added child's birth date into a full age-appropriate policy in one call.
  </Card>

  <Card title="Connect a platform" icon="plug" href="/guides/connect-a-platform">
    The full OAuth ceremony behind the `GET /oauth/profiles` roster this recipe diffs against.
  </Card>

  <Card title="Recover a disconnected platform" icon="plug-circle-exclamation" href="/guides/recipes/recover-disconnected-platform">
    The other reconciliation — a platform link that dropped rather than a roster that drifted.
  </Card>

  <Card title="Change a rule and enforce it" icon="bolt" href="/guides/recipes/policy-change-and-enforce">
    Push policy to every connected platform once the new children have one.
  </Card>
</CardGroup>
