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

# Recover a disconnected platform

> Enforcement stopped reaching a console. Detect the dropped link from the compliance list, re-establish it, re-enforce, and confirm the platform is healthy again — the full disconnect-detect-recover loop, run live against the open sandbox.

A platform a family connected months ago silently stops receiving policy updates — the parent
revoked access, a token expired, the link was removed. Nothing errors loudly; enforcement just
stops landing. This recipe shows how to **detect** that a platform link is gone or unhealthy from
the compliance list, **re-establish** it, **re-enforce** the current policy, and **confirm** the
platform is caught up.

```mermaid theme={null}
flowchart LR
  A["GET /families/{id}/compliance<br/>audit the links"] --> B{"link present<br/>& enforced?"}
  B -->|missing| C["POST /compliance<br/>re-establish"]
  B -->|stale| C
  C --> D["POST /children/{id}/enforce<br/>push current policy"]
  D --> E["GET /families/{id}/compliance<br/>confirm fresh enforcement"]
```

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.

<Info>
  **Sandbox-first.** These calls run against the open sandbox with no credential. In production the
  compliance endpoints are family-scoped and require a `phosra_live_…` key whose caller is a member
  of the family — the request shapes are identical.
</Info>

## Before you start

This recipe assumes a family that has connected at least one platform. If you ran
[Change a rule and enforce it](/guides/recipes/policy-change-and-enforce) you already have one; the
IDs below are from a family with an Xbox and a PlayStation connected. Set the base URL and IDs once:

```bash theme={null}
export PHOSRA_BASE="https://phosra-api-sandbox-production.up.railway.app"
export FAMILY_ID="9987bef4-a47a-49e2-a292-55da17ae6837"
export CHILD_ID="cd840d71-4956-49c4-91b4-a1cf0beb44e6"
```

<Steps>
  <Step title="Audit the family's platform links">
    [`GET /families/{familyID}/compliance`](/api-reference/compliance/list-family-compliance) lists
    every platform the family has connected, with two health fields per link: `last_enforcement_at` and
    `last_enforcement_status`. This is your disconnect detector.

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

      ```typescript TypeScript theme={null}
      const BASE = "https://phosra-api-sandbox-production.up.railway.app";
      const familyId = "9987bef4-a47a-49e2-a292-55da17ae6837";

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

      // A link needs recovery if it is missing entirely, or has never been enforced,
      // or its last enforcement was not clean:
      const needsRecovery = (l: any) =>
        !l.last_enforcement_at || l.last_enforcement_status !== "completed";
      ```

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

      BASE = "https://phosra-api-sandbox-production.up.railway.app"
      family_id = "9987bef4-a47a-49e2-a292-55da17ae6837"

      links = requests.get(f"{BASE}/api/v1/families/{family_id}/compliance", timeout=30).json()

      def needs_recovery(l):
          return (not l.get("last_enforcement_at")) or l.get("last_enforcement_status") != "completed"
      ```
    </CodeGroup>

    Live `200`. Here the PlayStation link has **no `last_enforcement_at`** — it was re-connected but
    never re-enforced, so the console is not yet on the current policy. That is the drop signal:

    ```json theme={null}
    [
      {
        "id": "73167ca9-c154-44f5-9ee9-1c7399d7f31c",
        "family_id": "9987bef4-a47a-49e2-a292-55da17ae6837",
        "platform_id": "xbox",
        "status": "verified",
        "last_enforcement_at": "2026-07-06T11:23:59.246117Z",
        "last_enforcement_status": "partial",
        "verified_at": "2026-07-06T11:23:58.766765Z"
      },
      {
        "id": "f499f980-4d06-42ff-9702-4da3df15d024",
        "family_id": "9987bef4-a47a-49e2-a292-55da17ae6837",
        "platform_id": "playstation",
        "status": "verified",
        "verified_at": "2026-07-06T11:24:22.116344Z"
      }
    ]
    ```

    | Signal on a link                                                 | Meaning                                 | Action                                         |
    | ---------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------- |
    | Platform **absent** from the list                                | The link was removed (revoked/expired). | Re-establish it (next step).                   |
    | No `last_enforcement_at`                                         | Connected but never enforced since.     | Re-enforce (step 3).                           |
    | `last_enforcement_status: "partial"`                             | Some rules did not fully land.          | Re-enforce, then read the per-platform report. |
    | `status: "verified"` + recent `last_enforcement_at: "completed"` | Healthy — no action.                    | —                                              |

    <Note>
      A completely dropped platform is simply **not in this array**. If a family used to have three
      consoles and the list returns two, the third is your disconnected link — re-establish it with the
      same `POST /compliance` you used to connect it originally.
    </Note>
  </Step>

  <Step title="Re-establish the link">
    If the platform is gone, [`POST /api/v1/compliance`](/api-reference/compliance/post-compliance)
    re-connects it with a fresh credential. (If the stale link still exists, remove it first with
    [`DELETE /api/v1/compliance/{linkID}`](/api-reference/compliance/disconnect-compliance) — shown in
    the accordion — then re-create it, so you are not left with two links for one platform.)

    <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\": \"playstation\",
          \"credentials\": \"fresh-token-after-reconnect\"
        }"
      ```

      ```typescript TypeScript theme={null}
      const relink = await (
        await fetch(`${BASE}/api/v1/compliance`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            family_id: familyId,
            platform_id: "playstation",
            credentials: "fresh-token-after-reconnect",
          }),
        })
      ).json();
      // relink.status === "verified"
      ```

      ```python Python theme={null}
      relink = requests.post(
          f"{BASE}/api/v1/compliance",
          json={
              "family_id": family_id,
              "platform_id": "playstation",
              "credentials": "fresh-token-after-reconnect",
          },
          timeout=30,
      ).json()
      # relink["status"] == "verified"
      ```
    </CodeGroup>

    Live `201` — a fresh link, verified, with a new `id`:

    ```json theme={null}
    {
      "id": "f499f980-4d06-42ff-9702-4da3df15d024",
      "family_id": "9987bef4-a47a-49e2-a292-55da17ae6837",
      "platform_id": "playstation",
      "status": "verified",
      "verified_at": "2026-07-06T11:24:22.11634477Z"
    }
    ```

    <Accordion title="Tearing down the stale link first — DELETE /compliance/{linkID}">
      When the old link still exists (its credential is dead but the row remains), remove it before
      re-creating so the family has exactly one link per platform:

      ```bash cURL theme={null}
      curl -s -o /dev/null -w "%{http_code}\n" \
        -X DELETE "$PHOSRA_BASE/api/v1/compliance/$LINK_ID"
      # → 204   (link removed; no further enforcement is pushed to it)
      ```

      | Status | Meaning                                                                      |
      | :----: | ---------------------------------------------------------------------------- |
      |  `204` | Link removed.                                                                |
      |  `404` | `compliance link not found` — already gone; safe to proceed to re-create.    |
      |  `403` | `not a member of this family` — production only; supply a family-scoped key. |

      Deleting an unknown id is a no-op you can rely on: the live sandbox returns
      `{"error":"Not Found","message":"compliance link not found","code":404}`.
    </Accordion>
  </Step>

  <Step title="Re-enforce the current policy">
    A re-established link starts empty — it has no rules until you push them.
    [`POST /children/{childID}/enforce`](/api-reference/enforcement/post-children-enforce) pushes the
    child's active policy to **every** connected platform, including the one you just recovered.

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

      ```typescript TypeScript theme={null}
      const job = await (
        await fetch(`${BASE}/api/v1/children/cd840d71-4956-49c4-91b4-a1cf0beb44e6/enforce`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: "{}",
        })
      ).json();
      const jobId: string = job.id; // job.status === "running"
      ```

      ```python Python theme={null}
      job = requests.post(
          f"{BASE}/api/v1/children/cd840d71-4956-49c4-91b4-a1cf0beb44e6/enforce",
          json={}, timeout=30,
      ).json()
      job_id = job["id"]  # job["status"] == "running"
      ```
    </CodeGroup>

    The job finishes in well under a second; poll
    [`GET /enforcement/jobs/{jobID}`](/api-reference/enforcement/get-enforcement-job) until it reaches a
    terminal status. Live terminal job:

    ```json theme={null}
    {
      "id": "81dd4f27-f7ef-42a5-bc56-6f15752237f3",
      "status": "partial",
      "started_at": "2026-07-06T11:26:56.324739Z",
      "completed_at": "2026-07-06T11:26:56.497458Z"
    }
    ```

    <Note>
      A `partial` job status means **at least one rule did not fully land** on some platform — the job
      still completed and every other rule was applied. Open the per-platform report
      (`GET /enforcement/jobs/{jobID}/results`) to see exactly which category needs attention; the
      [Change a rule and enforce it](/guides/recipes/policy-change-and-enforce) recipe walks that report
      in full. It is not a reason to re-run the enforce blindly.
    </Note>
  </Step>

  <Step title="Confirm the platform is caught up">
    Re-run the audit from step 1. Both links now carry a fresh `last_enforcement_at` — the recovered
    PlayStation is back on the current policy:

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

      ```typescript TypeScript theme={null}
      const after = await (
        await fetch(`${BASE}/api/v1/families/${familyId}/compliance`)
      ).json();
      // every link now has a recent last_enforcement_at
      ```

      ```python Python theme={null}
      after = requests.get(f"{BASE}/api/v1/families/{family_id}/compliance", timeout=30).json()
      # every link now has a recent last_enforcement_at
      ```
    </CodeGroup>

    Live `200` — the PlayStation link now has `last_enforcement_at`, matching the Xbox:

    ```json theme={null}
    [
      {
        "id": "73167ca9-c154-44f5-9ee9-1c7399d7f31c",
        "platform_id": "xbox",
        "status": "verified",
        "last_enforcement_at": "2026-07-06T11:26:56.407916Z",
        "last_enforcement_status": "partial"
      },
      {
        "id": "f499f980-4d06-42ff-9702-4da3df15d024",
        "platform_id": "playstation",
        "status": "verified",
        "last_enforcement_at": "2026-07-06T11:26:56.492301Z",
        "last_enforcement_status": "partial"
      }
    ]
    ```

    The dropped platform is reconnected and enforcing again. `last_enforcement_at` moving forward for
    every link is the recovery you were confirming.
  </Step>
</Steps>

## The whole flow at a glance

| # | Step         | Call                                 | Live result                              |
| - | ------------ | ------------------------------------ | ---------------------------------------- |
| 1 | Audit links  | `GET /families/{id}/compliance`      | PlayStation has no `last_enforcement_at` |
| 2 | Re-establish | `POST /compliance`                   | link `f499f980…`, `verified`             |
| 3 | Re-enforce   | `POST /children/{id}/enforce` → poll | job `81dd4f27…`, terminal                |
| 4 | Confirm      | `GET /families/{id}/compliance`      | both links freshly enforced              |

## Next steps

<CardGroup cols={2}>
  <Card title="Disconnect & reconnect" icon="link-slash" href="/guides/disconnect-reconnect">
    The OAuth-connect mirror: handling a declined consent and re-approving cleanly.
  </Card>

  <Card title="Change a rule and enforce it" icon="bolt" href="/guides/recipes/policy-change-and-enforce">
    Read the per-platform enforcement report — what `applied`, `guided`, and `partial` mean.
  </Card>

  <Card title="Reconcile family drift" icon="users-gear" href="/guides/recipes/reconcile-family-drift">
    Recover from the other kind of drift — a family roster that no longer matches the platform.
  </Card>

  <Card title="Back off and retry a 429" icon="clock-rotate-left" href="/guides/recipes/backoff-retry-429">
    When a call fails transiently rather than because the link is gone.
  </Card>
</CardGroup>
