> ## 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.

# End-to-end walkthrough

> The full family lifecycle in one page — create a family, browse the catalog, link two platforms, unlink one, and prove per-service isolation. Every call is run live against the open sandbox with real requests and responses, and every screenshot follows the same child (Ruby) and the same streaming link the code makes.

This is the whole journey a family takes through Phosra, start to finish, on a single page.
You will create a family, browse the platform catalog, **link two platforms**, unlink one of
them, watch the other survive untouched, and relink the first — all against the public sandbox.

Every request and response below is **verbatim live output**, captured while writing this page
by running these exact calls against
`https://phosra-api-sandbox-production.up.railway.app`. No API key, nothing to install.

<Info>
  **Sandbox-first.** These calls run against the open partner sandbox with no credential. Nothing
  you create touches a real family. The shapes are identical in production — you swap the base URL
  for `https://prodapi.phosra.com` and authenticate with a `phosra_live_…` key.
</Info>

There is one interactive path — the **OAuth connect ceremony** — where a real parent approves in
a browser. Its consent page belongs to the sandbox reference provider and ships its own fixed demo
profiles:

<Frame caption="The live sandbox consent page, captured from the running census. This screen belongs to the OAuth connect path (see 'Connect a platform'), not the direct-compliance walkthrough below. The profiles it lists — Mia, Leo, Ava — are the reference provider's built-in sandbox demos, separate from the family you create in this guide.">
  <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough-consent-live.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=054aa3abebfe4d5913ef737fa2223e14" alt="Phosra sandbox consent page titled 'Connect your family to this app?' with a 'Sandbox reference provider' tag, listing the reference provider's built-in demo child profiles Mia, Leo, and Ava, and Approve / Deny buttons." width="920" height="720" data-path="images/walkthrough-consent-live.jpg" />
</Frame>

## The ten phases

<Steps>
  <Step title="Create the family" />

  <Step title="Browse the catalog" />

  <Step title="Link the first platform (Notflix)" />

  <Step title="Confirm the link" />

  <Step title="Link a second platform (Pixagram)" />

  <Step title="See both managed" />

  <Step title="Unlink Notflix" />

  <Step title="Confirm release — and isolation" />

  <Step title="The survivor is untouched" />

  <Step title="Relink Notflix" />
</Steps>

The load-bearing idea is phases 7–9: **unlinking one platform is scoped to that link alone.**
Deleting the Notflix link does not disturb the second link, and that platform keeps verifying.
There is no cross-service bleed.

## The same journey, in a real app

Before the raw API calls, here is what the ten phases *look like* to a family. Every screen below
is a **real screenshot** captured from Propagate — a reference parental-control app built on this
exact sandbox API — and from a platform's own website. Nothing is mocked or drawn.

<Info>
  **One cast, two names.** The screenshots and the code below follow the **same child** and the
  **same streaming link** — only Propagate's playful brand names differ from the public catalog's real
  platform ids. Follow the code and you land on the exact UI state each frame shows.

  | In the screenshots (Propagate)  | In the API calls below   | The bridge                                                                                                                                                                                                                                           |
  | ------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | **Ruby**, the child             | `child_name: "Ruby"`     | The same child, end to end.                                                                                                                                                                                                                          |
  | **Notflix** (streaming)         | `platform_id: "netflix"` | Notflix is Propagate's stand-in brand for the catalog's **Netflix** — same streaming service, same link/unlink/relink target.                                                                                                                        |
  | **Pixagram** (a second service) | `platform_id: "xbox"`    | Pixagram is a social app. The public catalog ships real platform ids and has **no social equivalent**, so the API links `xbox` (a gaming platform) as the second, unrelated service. Both prove the identical point: unlink one, the other survives. |
</Info>

<Steps>
  <Step title="Create the family">
    The family lives in the app: two children (Ruby, Mateo) and the parent as owner. This guide's API
    path provisions **Ruby** — the child every later frame follows. This is the `POST /setup/quick`
    result — a family plus a child plus a starter policy — rendered for a human.

    <Frame caption="Phase 1 · The family. Ruby carries a 'Relaxed — a little above age 8' strictness label; Mateo is a second child in the app. The parent is the account owner. Ruby is what the setup/quick response below backs.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-01-family.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=03dd7172a65a27b93e20f23975638d7c" alt="Propagate iOS Family screen. A KIDS section lists Ruby ('Relaxed — a little above age 8') and Mateo ('Content suited to age 11'), each with an initial avatar. A PARENTS section shows the signed-in parent tagged 'You' and 'Owner', with 'Invite co-parent' and 'Join a family with a code' actions. A bottom tab bar has Home, Family (selected), Services, Settings." width="1206" height="2622" data-path="images/walkthrough/ff-01-family.jpg" />
    </Frame>
  </Step>

  <Step title="Browse the catalog">
    The catalog of connectable services, grouped by category. `GET /platforms` returns this list.

    <Frame caption="Phase 2 · The catalog. Services are grouped Social / Streaming / Gaming, mirroring the categories GET /platforms returns. The green 'Enforced' badge on Notflix is a real residue of an earlier session on this device — not a preview of this guide's family. A freshly created family (the one the API builds below) shows no badges until a link actually confirms; a badge only ever appears after the platform verifies, never as a hopeful default.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-02-catalog.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=6ae5d60d18836bdcfe60b02261df4723" alt="Propagate 'Add a service' catalog. SOCIAL lists Snaptr and Pixagram; STREAMING lists Notflix with a green checkmark 'Enforced' badge left over from a prior session; GAMING lists Bloxby. Each row has an icon, a one-line description of what it locks down, and a chevron." width="1206" height="2622" data-path="images/walkthrough/ff-02-catalog.jpg" />
    </Frame>
  </Step>

  <Step title="Link the first platform (Notflix)">
    The link ceremony — the app-side equivalent of `POST /compliance` against `platform_id: "netflix"`.
    It states, up front, exactly which rules the platform will apply and confirm before anything turns
    green.

    <Frame caption="Phase 3 · The link ceremony. Under the co-branded Phosra · OCSS header, the app lists the four rules Notflix will apply and verify. The footer is explicit: 'Shown as Enforced only after Notflix confirms — never a fake green.'">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-03-link-ceremony.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=a86c89e79ccaacfbc943c93d36d0be29" alt="Propagate 'Connect Notflix' screen with a 'phosra · OCSS' header. A card titled 'APPLIED & VERIFIED ON NOTFLIX' lists four rules with checkmarks: cap maturity rating for the child's profile, block specific titles by ID, lock the child into a kids profile, disable autoplay of the next episode. A green 'Continue' button sits at the bottom above the disclaimer about never showing a fake green." width="1206" height="2622" data-path="images/walkthrough/ff-03-link-ceremony.jpg" />
    </Frame>
  </Step>

  <Step title="Confirm the link">
    The link's detail screen. This is the `status: "verified"` from the compliance response, shown as
    a family sees it: **Enforced**, connected today, last synced *just now*.

    <Frame caption="Phase 4 · Confirmed. Protection type 'Enforced', connected July 5 2026, last sync 'Applied & verified just now', with the applied rules listed underneath — the human-readable form of the verified compliance link the API returns for netflix.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-04-linked.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=79dad33a3fdd6281be605af04d2c8f83" alt="Propagate Notflix detail screen. A green shield 'Enforced' card reads 'Applied and verified via secure OAuth — settings are written and confirmed through Notflix's API.' A Status section shows Protection type: Enforced, Connected: July 5 2026, Last sync: 'Applied & verified just now'. An Applied Rules section lists the maturity-cap, block-titles, and kids-profile-lock rules with green checkmarks." width="1206" height="2622" data-path="images/walkthrough/ff-04-linked.jpg" />
    </Frame>
  </Step>

  <Step title="Link a second platform (Pixagram)">
    A second service — **Pixagram**, a social app — is linked. The Services dashboard now reads
    **2 of 2 enforced**, the two-managed-platforms state the second `POST /compliance` produces.
    (In the API calls this second service is `xbox`: the catalog has no social equivalent for Pixagram,
    so xbox stands in — see the cast note above.)

    <Frame caption="Phase 5 · Two managed. The dashboard shows 2/2 enforced — Notflix and Pixagram, each '4 rules applied · Applied & verified just now'. The footer defines the honest distinction: Enforced = we apply and check it; Guided = you set it up, we can't verify.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-05-second-link.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=bc2c02e5199907bfa06de6ac70d370db" alt="Propagate Services screen. A purple 'Services to Protect' header card reads '2 / 2' with a '2 ENFORCED' pill and a full green progress bar. Below, a SET UP list shows Notflix (Enforced, 4 rules applied) and Pixagram (Enforced, 4 rules applied), each 'Applied & verified just now', plus an 'Add a service' button." width="1206" height="2622" data-path="images/walkthrough/ff-05-second-link.jpg" />
    </Frame>
  </Step>

  <Step title="See it enforced on the platform">
    Proof that the link is not cosmetic: **Ruby's** profile **on the streaming platform's own website**
    carries a *Managed via Phosra* badge. The rule is enforced where it actually matters.

    <Frame caption="Phase 6 · Enforced at the source. On Notflix's own 'Manage profile and preferences' page, Ruby's linked profile carries a 'Managed via Phosra' badge — proof the connection is live on the platform itself, not just a status inside the parent app. (The 'age 17' on the row is the Notflix reference sandbox's built-in demo label for this profile, not Ruby's Phosra age — she is 8, capped at PG / 7+ by the relaxed preset in Phase 1. The badge, not that number, is what this frame proves.)">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-06-managed-on-platform.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=10078b7f23872c8655d5c1819f6b5356" alt="The Notflix web app 'Manage profile and preferences' page in dark mode, signed in as ruby-family-f5@example.com. Ruby's profile row — labeled 'Kids profile · age 17', the reference sandbox's demo default — carries a green 'Managed via Phosra' badge. A PREFERENCES section lists Languages, Adjust Parental Controls, Subtitle appearance, and Playback settings." width="1280" height="900" data-path="images/walkthrough/ff-06-managed-on-platform.jpg" />
    </Frame>
  </Step>

  <Step title="Unlink one platform">
    The family cancels one service. The confirmation is scoped and explicit — it will revoke *only*
    this platform's rules. This is the app-side `DELETE /compliance/{linkID}` for the Notflix link.

    <Frame caption="Phase 7 · Unlink, scoped. The 'Disconnect Notflix?' dialog promises to remove this connection and revoke the rules applied to Notflix only. The screen behind it reminds you removing one child revokes only their rules — the others stay connected.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-07-unlink.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=d64daa29edb9fb49a6dee32b76faecbe" alt="Propagate Notflix detail screen with a 'Disconnect Notflix?' confirmation dialog: 'We'll remove this connection and revoke the rules applied to Notflix.' with Cancel and red Disconnect buttons. Behind it, a 'Children on Notflix' section lists Ruby ('4 rules applied') with a Remove action and the note 'Removing one child revokes only their rules — the others stay connected.'" width="1206" height="2622" data-path="images/walkthrough/ff-07-unlink.jpg" />
    </Frame>
  </Step>

  <Step title="Confirm release">
    The unlinked platform reports **Disconnected — rules are no longer applied**, and the platform
    itself *confirmed* the removal. Clean teardown, not a silent drop. A **Reconnect** button waits.

    <Frame caption="Phase 8 · Released. Notflix now reads 'Disconnected · Rules are no longer applied via Phosra · Notflix confirmed removal'. The rule is provably gone at the source, and a Reconnect button offers the relink path.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-08-released.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=790067e6f6707df3251c57c4e6e3a4fb" alt="Propagate Notflix detail screen showing a teal 'Enforceable' capability card and a 'Disconnected' status card: 'Rules are no longer applied via Phosra. Notflix confirmed removal · July 5, 2026.' A large green 'Reconnect' button sits below with the caption 'Reconnect to re-apply Phosra rules on Notflix.'" width="1206" height="2622" data-path="images/walkthrough/ff-08-released.jpg" />
    </Frame>
  </Step>

  <Step title="The other platform survived">
    The isolation guarantee, made visible: after unlinking one service, the dashboard reads **1 of 1
    enforced** — the *other* platform is untouched and still verifying. No cross-service bleed. (In the
    screenshots the survivor is **Pixagram**; in the API calls it is `xbox` — the same second, unrelated
    service standing in for Pixagram, see the cast note above.)

    <Frame caption="Phase 9 · Isolation. With Notflix gone, the dashboard drops to 1/1 enforced — the second service is untouched, still '4 rules applied · Applied & verified just now'. This is the app-side proof of the same isolation the /verify call proves at the API.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-08b-pixagram-survived.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=5e6151cb1690eedf9d5b82f124331c05" alt="Propagate Services screen after unlinking Notflix. The 'Services to Protect' header now reads '1 / 1' with a '1 ENFORCED' pill and a full green bar. The SET UP list shows only Pixagram (Enforced, 4 rules applied, 'Applied & verified just now'), plus an 'Add a service' button." width="1206" height="2622" data-path="images/walkthrough/ff-08b-pixagram-survived.jpg" />
    </Frame>
  </Step>

  <Step title="Relink">
    The family resubscribes and relinks. The platform confirms and protection resumes on a fresh link —
    the app-side `POST /compliance` for `netflix` again, closing the loop.

    <Frame caption="Phase 10 · Relinked. A success screen confirms 'Notflix connected · Protecting Ruby', timestamped — the relink completed and enforcement is live again on a brand-new link.">
      <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough/ff-09-relink.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=d461274e61325db946ae23302e926228" alt="Propagate success screen with a large green shield checkmark. Headline 'Notflix connected', subtext 'Protecting Ruby', timestamp 'Jul 5 at 10:07 PM', and a green 'Done' button." width="1206" height="2622" data-path="images/walkthrough/ff-09-relink.jpg" />
    </Frame>
  </Step>
</Steps>

Those are the ten phases as a family experiences them. The rest of this page is the same ten
phases as **API calls** — every request and response run live against the sandbox, following the
same child (**Ruby**) and the same streaming link (**Notflix** → `netflix`).

## Before you start

Set the base URL once so every step is copy-paste:

```bash theme={null}
export PHOSRA_BASE="https://phosra-api-sandbox-production.up.railway.app"
```

This guide uses the **direct compliance link** — the simplest full-lifecycle path, where you
present a platform credential to Phosra. For the interactive **OAuth connect ceremony** (the
consent page above, with `authorize_url` → `token` → `profiles`), see
[Connect a platform](/guides/connect-a-platform). The link/unlink/relink lifecycle below is the
same either way.

<Steps>
  <Step title="Create the family">
    One call onboards a family, a child, and an age-appropriate starter policy. `setup/quick` needs
    only a child name and birth date. We create **Ruby** — the same child the screenshots follow
    ([Phase 1](#the-same-journey-in-a-real-app)).

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST "$PHOSRA_BASE/api/v1/setup/quick" \
        -H "Content-Type: application/json" \
        -d '{
          "child_name": "Ruby",
          "birth_date": "2018-02-14",
          "family_name": "Ruby'\''s Family",
          "strictness": "relaxed"
        }'
      ```

      ```typescript TypeScript theme={null}
      const BASE = "https://phosra-api-sandbox-production.up.railway.app";

      const res = await fetch(`${BASE}/api/v1/setup/quick`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          child_name: "Ruby",
          birth_date: "2018-02-14",
          family_name: "Ruby's Family",
          strictness: "relaxed",
        }),
      });
      const setup = await res.json();
      const familyId = setup.family.id; // carry this into every step below
      ```

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

      BASE = "https://phosra-api-sandbox-production.up.railway.app"

      setup = requests.post(
          f"{BASE}/api/v1/setup/quick",
          json={
              "child_name": "Ruby",
              "birth_date": "2018-02-14",
              "family_name": "Ruby's Family",
              "strictness": "relaxed",
          },
          timeout=30,
      ).json()
      family_id = setup["family"]["id"]  # carry this into every step below
      ```
    </CodeGroup>

    The response returns the family, the child, and a fully-populated starter policy (**20 rules**
    seeded from the `relaxed` preset for an 8-year-old, with the content cap at PG / 7+). Truncated to
    the parts you carry forward:

    ```json theme={null}
    {
      "family": {
        "id": "07617e71-c05d-48a8-942b-53c06b04a2db",
        "name": "Ruby's Family",
        "created_at": "2026-07-06T09:02:10.028096913Z"
      },
      "child": {
        "id": "5d8d11cb-a5c7-4598-8972-85ef1f248350",
        "family_id": "07617e71-c05d-48a8-942b-53c06b04a2db",
        "name": "Ruby",
        "birth_date": "2018-02-14T00:00:00Z"
      },
      "policy": {
        "id": "c9cfef3c-c556-4b58-bab5-e6f4fcb3ee36",
        "name": "Ruby's Protection Policy",
        "status": "active"
      },
      "rule_summary": {
        "total_rules_enabled": 20,
        "content_rating": "PG",
        "web_filter_level": "strict",
        "screen_time_minutes": 117
      }
    }
    ```

    Keep `family.id` — every remaining step is scoped to it.

    ```bash theme={null}
    export FAMILY_ID="07617e71-c05d-48a8-942b-53c06b04a2db"
    ```

    <Accordion title="Fields & errors">
      | Field         | Type   | Notes                                                               |
      | ------------- | ------ | ------------------------------------------------------------------- |
      | `child_name`  | string | **Required.** The child's display name.                             |
      | `birth_date`  | date   | **Required.** `YYYY-MM-DD`. Drives the age-appropriate rule preset. |
      | `family_name` | string | Optional. Defaults to `"{child_name}'s Family"`.                    |
      | `strictness`  | enum   | `recommended` (default), `strict`, or `relaxed`.                    |

      A missing `child_name` or `birth_date` returns `400 { "message": "child_name is required" }`.
      Re-running with the same `(family, child_name, birth_date)` de-dupes rather than creating a
      duplicate child.
    </Accordion>
  </Step>

  <Step title="Browse the catalog">
    `GET /platforms` returns everything a family can connect — streaming apps, game consoles, DNS
    filters, devices, operating systems. No auth.

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

      ```typescript TypeScript theme={null}
      const platforms = await (
        await fetch(`${BASE}/api/v1/platforms`)
      ).json();
      // 22 connectable platforms; pick two to link
      ```

      ```python Python theme={null}
      platforms = requests.get(f"{BASE}/api/v1/platforms", timeout=30).json()
      # 22 connectable platforms; pick two to link
      ```
    </CodeGroup>

    The live sandbox returns **22 platforms**. Two of them — `netflix` (the streaming service the
    screenshots brand **Notflix**) and `xbox` (the second, unrelated service) — are what we link next:

    ```json theme={null}
    [
      { "id": "netflix", "name": "Netflix", "category": "streaming", "enabled": true },
      { "id": "xbox",    "name": "Xbox",    "category": "gaming",    "enabled": true }
    ]
    ```
  </Step>

  <Step title="Link Notflix (netflix)">
    Linking is a single `POST /compliance` with the family, the platform, and a platform credential.
    In the sandbox the credential is a demo token; in production it is the real access token you
    obtained from the [connect ceremony](/guides/connect-a-platform). This is the API form of the
    link ceremony in [Phase 3](#the-same-journey-in-a-real-app).

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST "$PHOSRA_BASE/api/v1/compliance" \
        -H "Content-Type: application/json" \
        -d "{
          \"family_id\": \"$FAMILY_ID\",
          \"platform_id\": \"netflix\",
          \"credentials\": \"sandbox-demo-token\"
        }"
      ```

      ```typescript TypeScript theme={null}
      const netflix = await (
        await fetch(`${BASE}/api/v1/compliance`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            family_id: familyId,
            platform_id: "netflix",
            credentials: "sandbox-demo-token",
          }),
        })
      ).json();
      const netflixLinkId = netflix.id; // you unlink and relink with this
      ```

      ```python Python theme={null}
      netflix = requests.post(
          f"{BASE}/api/v1/compliance",
          json={
              "family_id": family_id,
              "platform_id": "netflix",
              "credentials": "sandbox-demo-token",
          },
          timeout=30,
      ).json()
      netflix_link_id = netflix["id"]  # you unlink and relink with this
      ```
    </CodeGroup>

    The response is the new link. It comes back **`verified`** — Phosra confirmed the credential
    against the platform. This is the exact state [Phase 4](#the-same-journey-in-a-real-app) renders
    as **Enforced · Applied & verified just now**:

    ```json theme={null}
    {
      "id": "5b6d14f3-3ea5-4ae0-a98e-baced770d1df",
      "family_id": "07617e71-c05d-48a8-942b-53c06b04a2db",
      "platform_id": "netflix",
      "status": "verified",
      "verified_at": "2026-07-06T09:02:28.396962605Z"
    }
    ```

    ```bash theme={null}
    export NETFLIX_LINK_ID="5b6d14f3-3ea5-4ae0-a98e-baced770d1df"
    ```

    <Accordion title="Fields & errors">
      | Field         | Type   | Notes                                                                         |
      | ------------- | ------ | ----------------------------------------------------------------------------- |
      | `family_id`   | uuid   | **Required.** From step 1.                                                    |
      | `platform_id` | string | **Required.** A catalog `id` from step 2 (e.g. `netflix`).                    |
      | `credentials` | string | **Required.** Sandbox: any demo token. Production: the platform access token. |

      An unknown `platform_id` or malformed `family_id` returns `400`. A duplicate active link for the
      same `(family, platform)` returns the existing link rather than a second row.
    </Accordion>
  </Step>

  <Step title="Confirm the link">
    `GET /families/{id}/compliance` lists every active link for the family. Right now there is one:

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

      ```typescript TypeScript theme={null}
      const links = await (
        await fetch(`${BASE}/api/v1/families/${familyId}/compliance`)
      ).json();
      ```

      ```python Python theme={null}
      links = requests.get(
          f"{BASE}/api/v1/families/{family_id}/compliance", timeout=30
      ).json()
      ```
    </CodeGroup>

    ```json theme={null}
    [
      {
        "id": "5b6d14f3-3ea5-4ae0-a98e-baced770d1df",
        "family_id": "07617e71-c05d-48a8-942b-53c06b04a2db",
        "platform_id": "netflix",
        "status": "verified",
        "verified_at": "2026-07-06T09:02:28.396962Z"
      }
    ]
    ```
  </Step>

  <Step title="Link the second service (xbox)">
    Repeat the link for a second, unrelated platform. The screenshots show this as a second service
    (**Pixagram**, a social app) reaching **2 of 2 enforced** in [Phase 5](#the-same-journey-in-a-real-app);
    the public catalog has no social equivalent for Pixagram, so the API links `xbox` — same call,
    different `platform_id`.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST "$PHOSRA_BASE/api/v1/compliance" \
        -H "Content-Type: application/json" \
        -d "{
          \"family_id\": \"$FAMILY_ID\",
          \"platform_id\": \"xbox\",
          \"credentials\": \"sandbox-demo-token\"
        }"
      ```

      ```typescript TypeScript theme={null}
      const xbox = await (
        await fetch(`${BASE}/api/v1/compliance`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            family_id: familyId,
            platform_id: "xbox",
            credentials: "sandbox-demo-token",
          }),
        })
      ).json();
      const xboxLinkId = xbox.id;
      ```

      ```python Python theme={null}
      xbox = requests.post(
          f"{BASE}/api/v1/compliance",
          json={
              "family_id": family_id,
              "platform_id": "xbox",
              "credentials": "sandbox-demo-token",
          },
          timeout=30,
      ).json()
      xbox_link_id = xbox["id"]
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "96d4b117-7711-4527-9c4a-e6e13b7094ff",
      "family_id": "07617e71-c05d-48a8-942b-53c06b04a2db",
      "platform_id": "xbox",
      "status": "verified",
      "verified_at": "2026-07-06T09:02:28.913726803Z"
    }
    ```

    ```bash theme={null}
    export XBOX_LINK_ID="96d4b117-7711-4527-9c4a-e6e13b7094ff"
    ```
  </Step>

  <Step title="See both managed">
    The family now manages **two** verified links — the `2/2 enforced` dashboard state of
    [Phase 5](#the-same-journey-in-a-real-app). The same list call returns both:

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

      ```typescript TypeScript theme={null}
      const managed = await (
        await fetch(`${BASE}/api/v1/families/${familyId}/compliance`)
      ).json();
      // managed.length === 2  → [netflix, xbox]
      ```

      ```python Python theme={null}
      managed = requests.get(
          f"{BASE}/api/v1/families/{family_id}/compliance", timeout=30
      ).json()
      # len(managed) == 2  → [netflix, xbox]
      ```
    </CodeGroup>

    ```json theme={null}
    [
      {
        "id": "5b6d14f3-3ea5-4ae0-a98e-baced770d1df",
        "platform_id": "netflix",
        "status": "verified",
        "verified_at": "2026-07-06T09:02:28.396962Z"
      },
      {
        "id": "96d4b117-7711-4527-9c4a-e6e13b7094ff",
        "platform_id": "xbox",
        "status": "verified",
        "verified_at": "2026-07-06T09:02:28.913726Z"
      }
    ]
    ```
  </Step>

  <Step title="Unlink Notflix (netflix)">
    The family cancels Notflix — the [Phase 7](#the-same-journey-in-a-real-app) "Disconnect Notflix?"
    dialog. Delete **only** the Netflix link by its `linkID`. A successful teardown returns
    `204 No Content` — no body.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -o /dev/null -w "%{http_code}\n" \
        -X DELETE "$PHOSRA_BASE/api/v1/compliance/$NETFLIX_LINK_ID"
      # → 204
      ```

      ```typescript TypeScript theme={null}
      const res = await fetch(`${BASE}/api/v1/compliance/${netflixLinkId}`, {
        method: "DELETE",
      });
      // res.status === 204
      ```

      ```python Python theme={null}
      res = requests.delete(
          f"{BASE}/api/v1/compliance/{netflix_link_id}", timeout=30
      )
      # res.status_code == 204
      ```
    </CodeGroup>

    ```
    HTTP/1.1 204 No Content
    ```

    <Warning>
      The delete is keyed on the **link's** `id`, not the platform or the family. It removes exactly
      one link. This is what makes the next step true.
    </Warning>
  </Step>

  <Step title="Confirm release — and isolation">
    List the family's links again. Netflix is gone; **the xbox link is untouched** — the
    `1/1 enforced` survivor state of [Phase 9](#the-same-journey-in-a-real-app):

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

      ```typescript TypeScript theme={null}
      const afterUnlink = await (
        await fetch(`${BASE}/api/v1/families/${familyId}/compliance`)
      ).json();
      // afterUnlink.length === 1  → [xbox]
      ```

      ```python Python theme={null}
      after_unlink = requests.get(
          f"{BASE}/api/v1/families/{family_id}/compliance", timeout=30
      ).json()
      # len(after_unlink) == 1  → [xbox]
      ```
    </CodeGroup>

    ```json theme={null}
    [
      {
        "id": "96d4b117-7711-4527-9c4a-e6e13b7094ff",
        "family_id": "07617e71-c05d-48a8-942b-53c06b04a2db",
        "platform_id": "xbox",
        "status": "verified",
        "verified_at": "2026-07-06T09:02:28.913726Z"
      }
    ]
    ```

    <Frame caption="Per-service isolation, captured live: deleting the Netflix link (DELETE → 204) is scoped to that one linkID. The Xbox link is never touched and keeps verifying across the whole flow.">
      <img className="block dark:hidden" src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough-isolation-light.svg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=b71f3c7735890e7ecd9bd09e897e275e" alt="Two-lane timeline. The Netflix lane runs verified through link, second-link, and managed, is cut at 'unlink Netflix' (DELETE 204), shows 'released', then a new verified node at relink. The Xbox lane runs continuously verified across every phase, labeled 'unaffected, still verified'." width="940" height="430" data-path="images/walkthrough-isolation-light.svg" />

      <img className="hidden dark:block" src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/walkthrough-isolation-dark.svg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=61d440b675310d35acfefcd133c45180" alt="Two-lane timeline. The Netflix lane runs verified through link, second-link, and managed, is cut at 'unlink Netflix' (DELETE 204), shows 'released', then a new verified node at relink. The Xbox lane runs continuously verified across every phase, labeled 'unaffected, still verified'." width="940" height="430" data-path="images/walkthrough-isolation-dark.svg" />
    </Frame>
  </Step>

  <Step title="The survivor survived (xbox)">
    Isolation is not just "the xbox link is still listed" — it is "it still **works**." Verify the
    Xbox link after the Netflix teardown and it re-confirms clean:

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST "$PHOSRA_BASE/api/v1/compliance/$XBOX_LINK_ID/verify" \
        -H "Content-Type: application/json" -d '{}'
      ```

      ```typescript TypeScript theme={null}
      const check = await (
        await fetch(`${BASE}/api/v1/compliance/${xboxLinkId}/verify`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: "{}",
        })
      ).json();
      // check.status === "verified"
      ```

      ```python Python theme={null}
      check = requests.post(
          f"{BASE}/api/v1/compliance/{xbox_link_id}/verify",
          json={},
          timeout=30,
      ).json()
      # check["status"] == "verified"
      ```
    </CodeGroup>

    ```json theme={null}
    { "status": "verified" }
    ```

    Removing Netflix had zero effect on the Xbox link — a different platform's enforcement kept running
    the whole time. This is the guarantee real households depend on: cancelling one service never
    quietly drops protection on another.
  </Step>

  <Step title="Relink Notflix (netflix)">
    The family resubscribes — the [Phase 10](#the-same-journey-in-a-real-app) "Notflix connected ·
    Protecting Ruby" success screen. Relinking is just the link call again — and because the earlier
    delete fully released the old link, you get a **fresh** `linkID` (note it differs from step 3):

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST "$PHOSRA_BASE/api/v1/compliance" \
        -H "Content-Type: application/json" \
        -d "{
          \"family_id\": \"$FAMILY_ID\",
          \"platform_id\": \"netflix\",
          \"credentials\": \"sandbox-demo-token\"
        }"
      ```

      ```typescript TypeScript theme={null}
      const relinked = await (
        await fetch(`${BASE}/api/v1/compliance`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            family_id: familyId,
            platform_id: "netflix",
            credentials: "sandbox-demo-token",
          }),
        })
      ).json();
      // relinked.id !== netflixLinkId  → a brand-new link
      ```

      ```python Python theme={null}
      relinked = requests.post(
          f"{BASE}/api/v1/compliance",
          json={
              "family_id": family_id,
              "platform_id": "netflix",
              "credentials": "sandbox-demo-token",
          },
          timeout=30,
      ).json()
      # relinked["id"] != netflix_link_id  → a brand-new link
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "28163206-1de7-4d76-b40b-302a5296846b",
      "family_id": "07617e71-c05d-48a8-942b-53c06b04a2db",
      "platform_id": "netflix",
      "status": "verified",
      "verified_at": "2026-07-06T09:02:30.111746394Z"
    }
    ```

    The family is back to two managed platforms, with Netflix on a new link (`28163206…`, up from the
    released `5b6d14f3…`) and Xbox on the same one it has had the entire time.
  </Step>
</Steps>

## The whole flow at a glance

Every row below is one call you just ran, in order — the same **Ruby** family the screenshots
follow:

| #  | Phase                    | Call                               | Live result                                    |
| -- | ------------------------ | ---------------------------------- | ---------------------------------------------- |
| 1  | Create the family        | `POST /setup/quick`                | family `07617e71…`, child Ruby, 20-rule policy |
| 2  | Browse the catalog       | `GET /platforms`                   | 22 connectable platforms                       |
| 3  | Link Notflix (`netflix`) | `POST /compliance`                 | link `5b6d14f3…`, `verified`                   |
| 4  | Confirm the link         | `GET /families/{id}/compliance`    | `[netflix]`                                    |
| 5  | Link second (`xbox`)     | `POST /compliance`                 | link `96d4b117…`, `verified`                   |
| 6  | See both managed         | `GET /families/{id}/compliance`    | `[netflix, xbox]`                              |
| 7  | Unlink Notflix           | `DELETE /compliance/{linkID}`      | `204 No Content`                               |
| 8  | Confirm release          | `GET /families/{id}/compliance`    | `[xbox]` — Netflix gone                        |
| 9  | Survivor survived        | `POST /compliance/{linkID}/verify` | `{ "status": "verified" }`                     |
| 10 | Relink Notflix           | `POST /compliance`                 | new link `28163206…`, `verified`               |

## Next steps

<CardGroup cols={2}>
  <Card title="Connect a platform" icon="link" href="/guides/connect-a-platform">
    The interactive OAuth connect ceremony behind the consent page — `authorize_url` → token → profiles.
  </Card>

  <Card title="Disconnect & reconnect" icon="rotate" href="/guides/disconnect-reconnect">
    The decline path, the production teardown, and clean re-approval in depth.
  </Card>

  <Card title="Families & kids" icon="users" href="/guides/family-and-kids">
    Manage children, policies, and rules on the family you just created.
  </Card>

  <Card title="Test in the sandbox" icon="flask" href="/guides/test-in-sandbox">
    The seeded reference providers and Trust List these calls run against.
  </Card>
</CardGroup>
