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

# Change a rule and enforce it

> Tighten one rule on a live policy, push the change to a connected platform, and read back exactly which rules were applied, which were guided, and which the platform cannot enforce. Every call runs live against the open sandbox.

Setting a policy is half the job. The other half is **pushing a change to the platforms a family has
already connected** and knowing precisely what happened on each one. This recipe tightens a
screen-time limit, enforces it against a connected console, and reads back a per-platform report:
what was applied, what was *guided*, and what the platform simply can't do.

You will chain five endpoints:

```mermaid theme={null}
flowchart LR
  A["GET /children/{id}/policies<br/>find the active policy"] --> B["GET /policies/{id}/rules<br/>locate the rule"]
  B --> C["PUT /rules/{id}<br/>tighten the limit"]
  C --> D["POST /children/{id}/enforce<br/>push to platforms"]
  D --> E["GET /enforcement/jobs/{id}<br/>poll to completed"]
  E --> F["GET /enforcement/jobs/{id}/results<br/>per-platform report"]
```

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. The shapes are
  identical in production: swap the base URL for `https://prodapi.phosra.com` and add a
  `phosra_live_…` key.
</Info>

## Prerequisites

This recipe starts from **a child with an active policy and at least one connected platform**. If you
ran [Onboard a multi-child household](/guides/recipes/multi-child-household) you already have the
child and policy — the two calls below add a connected console (Xbox) so there is something to
enforce against. Set the base URL and IDs once:

```bash theme={null}
export PHOSRA_BASE="https://phosra-api-sandbox-production.up.railway.app"
export FAMILY_ID="af03190e-b31c-40c9-9cc4-f282a532326e"   # from the multi-child recipe
export CHILD_ID="acf4ce5d-d452-47b8-a78b-3b4ca86f50be"    # Ava, age 16, active policy

# Connect a console to the family so the enforce step has a target
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\" }"
```

```json theme={null}
{ "id": "bd220718-3c98-42f8-9d7f-31d82a0e88a3", "platform_id": "xbox", "status": "verified" }
```

<Note>
  Don't have a family yet? Run [Onboard a multi-child household](/guides/recipes/multi-child-household)
  first (three calls), or the [End-to-end walkthrough](/guides/end-to-end-walkthrough) for the full
  platform-link ceremony. Then come back with your own `CHILD_ID`.
</Note>

<Steps>
  <Step title="Find the child's active policy">
    [`GET /children/{childID}/policies`](/api-reference/policies/get-children-policies) lists a child's
    policies. Grab the `active` one — that is the policy enforcement reads from.

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

      ```typescript TypeScript theme={null}
      const BASE = "https://phosra-api-sandbox-production.up.railway.app";
      const childId = "acf4ce5d-d452-47b8-a78b-3b4ca86f50be";

      const policies = await (
        await fetch(`${BASE}/api/v1/children/${childId}/policies`)
      ).json();
      const active = policies.find((p: any) => p.status === "active");
      const policyId: string = active.id;
      ```

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

      BASE = "https://phosra-api-sandbox-production.up.railway.app"
      child_id = "acf4ce5d-d452-47b8-a78b-3b4ca86f50be"

      policies = requests.get(f"{BASE}/api/v1/children/{child_id}/policies", timeout=30).json()
      active = next(p for p in policies if p["status"] == "active")
      policy_id = active["id"]
      ```
    </CodeGroup>

    ```json theme={null}
    [
      {
        "id": "a3147895-b752-4d03-846c-35fd8fb4e77c",
        "child_id": "acf4ce5d-d452-47b8-a78b-3b4ca86f50be",
        "name": "Ava's Policy",
        "status": "active",
        "version": 20
      }
    ]
    ```

    ```bash theme={null}
    export POLICY_ID="a3147895-b752-4d03-846c-35fd8fb4e77c"
    ```
  </Step>

  <Step title="Locate the rule to change">
    [`GET /policies/{policyID}/rules`](/api-reference/rules/list-policy-rules) lists every rule on the
    policy. Find the `time_daily_limit` rule — it is currently **180 minutes** — and keep its `id`.

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

      ```typescript TypeScript theme={null}
      const rules = await (
        await fetch(`${BASE}/api/v1/policies/${policyId}/rules`)
      ).json();
      const limitRule = rules.find((r: any) => r.category === "time_daily_limit");
      const ruleId: string = limitRule.id; // config.daily_minutes === 180
      ```

      ```python Python theme={null}
      rules = requests.get(f"{BASE}/api/v1/policies/{policy_id}/rules", timeout=30).json()
      limit_rule = next(r for r in rules if r["category"] == "time_daily_limit")
      rule_id = limit_rule["id"]  # config["daily_minutes"] == 180
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "1a1e8d7d-e2fe-463b-9952-e560e5adf577",
      "policy_id": "a3147895-b752-4d03-846c-35fd8fb4e77c",
      "category": "time_daily_limit",
      "enabled": true,
      "config": { "daily_minutes": 180 }
    }
    ```

    ```bash theme={null}
    export RULE_ID="1a1e8d7d-e2fe-463b-9952-e560e5adf577"
    ```
  </Step>

  <Step title="Tighten the limit">
    [`PUT /rules/{ruleID}`](/api-reference/rules/update-rule) updates a single rule in place. Drop the
    daily limit from **180 to 120 minutes**.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X PUT "$PHOSRA_BASE/api/v1/rules/$RULE_ID" \
        -H "Content-Type: application/json" \
        -d '{ "enabled": true, "config": { "daily_minutes": 120 } }'
      ```

      ```typescript TypeScript theme={null}
      const updated = await (
        await fetch(`${BASE}/api/v1/rules/${ruleId}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ enabled: true, config: { daily_minutes: 120 } }),
        })
      ).json();
      // updated.config.daily_minutes === 120
      ```

      ```python Python theme={null}
      updated = requests.put(
          f"{BASE}/api/v1/rules/{rule_id}",
          json={"enabled": True, "config": {"daily_minutes": 120}},
          timeout=30,
      ).json()
      # updated["config"]["daily_minutes"] == 120
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "1a1e8d7d-e2fe-463b-9952-e560e5adf577",
      "category": "time_daily_limit",
      "enabled": true,
      "config": { "daily_minutes": 120 },
      "updated_at": "2026-07-06T08:45:54.003285438Z"
    }
    ```

    The policy is now updated, but nothing has reached the console yet. Editing a rule changes the
    **policy**; it does not touch the platform until you enforce.
  </Step>

  <Step title="Enforce the policy">
    [`POST /children/{childID}/enforce`](/api-reference/enforcement/post-children-enforce) pushes the
    child's current active policy to **every platform the family has connected**. It returns an
    enforcement **job** that runs asynchronously.

    <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/${childId}/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/{child_id}/enforce",
          json={},
          timeout=30,
      ).json()
      job_id = job["id"]  # job["status"] == "running"
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "67c763ff-efc9-4e27-99c7-fffe0fbac020",
      "child_id": "acf4ce5d-d452-47b8-a78b-3b4ca86f50be",
      "policy_id": "a3147895-b752-4d03-846c-35fd8fb4e77c",
      "trigger_type": "manual",
      "status": "running",
      "started_at": "2026-07-06T08:45:54.171964Z"
    }
    ```

    ```bash theme={null}
    export JOB_ID="67c763ff-efc9-4e27-99c7-fffe0fbac020"
    ```
  </Step>

  <Step title="Poll the job to completion">
    [`GET /enforcement/jobs/{jobID}`](/api-reference/enforcement/get-enforcement-job) returns the job's
    status. In the sandbox it finishes in well under a second; poll until `status` is `completed`.

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

      ```typescript TypeScript theme={null}
      async function waitForJob(id: string) {
        for (;;) {
          const j = await (await fetch(`${BASE}/api/v1/enforcement/jobs/${id}`)).json();
          if (j.status === "completed" || j.status === "failed") return j;
          await new Promise((r) => setTimeout(r, 500));
        }
      }
      const done = await waitForJob(jobId); // done.status === "completed"
      ```

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

      def wait_for_job(job_id: str):
          while True:
              j = requests.get(f"{BASE}/api/v1/enforcement/jobs/{job_id}", timeout=30).json()
              if j["status"] in ("completed", "failed"):
                  return j
              time.sleep(0.5)

      done = wait_for_job(job_id)  # done["status"] == "completed"
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "id": "67c763ff-efc9-4e27-99c7-fffe0fbac020",
      "status": "completed",
      "started_at": "2026-07-06T08:45:54.171964Z",
      "completed_at": "2026-07-06T08:45:54.315948Z"
    }
    ```
  </Step>

  <Step title="Read the per-platform report">
    [`GET /enforcement/jobs/{jobID}/results`](/api-reference/enforcement/get-enforcement-job-results)
    returns one result **per connected platform**. This is where you learn exactly what landed: Xbox
    applied 5 rules, guided several more, and flagged the categories a console can't enforce.

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

      ```typescript TypeScript theme={null}
      const results = await (
        await fetch(`${BASE}/api/v1/enforcement/jobs/${jobId}/results`)
      ).json();
      for (const r of results) {
        console.log(r.platform_id, r.status, `${r.rules_applied} applied`);
      }
      ```

      ```python Python theme={null}
      results = requests.get(
          f"{BASE}/api/v1/enforcement/jobs/{job_id}/results", timeout=30
      ).json()
      for r in results:
          print(r["platform_id"], r["status"], f"{r['rules_applied']} applied")
      ```
    </CodeGroup>

    ```json theme={null}
    [
      {
        "platform_id": "xbox",
        "status": "completed",
        "rules_applied": 5,
        "rules_skipped": 0,
        "rules_failed": 0,
        "details": {
          "profile_name": "Ava's Xbox Profile",
          "enforcement_mode": "manual_attested",
          "time_daily_limit": { "status": "guided", "limit_display": "2h 0m", "limit_minutes": 120 },
          "content_rating":   { "status": "guided", "max_rating": "PG-13", "blocked_above": ["R", "NC-17"] },
          "notification_curfew": { "status": "applied", "start": "22:00", "end": "06:00" },
          "monitoring_activity": { "status": "applied", "handler": "phosra_analytics" },
          "unenforceable_categories": [
            "addictive_design_control", "algo_feed_control", "privacy_profile_visibility",
            "targeted_ad_block", "web_filter_level", "web_safesearch"
          ]
        }
      }
    ]
    ```

    The new **120-minute** limit is right there in `time_daily_limit.limit_minutes` — your rule change
    made it to the platform. Read the three statuses this way:

    | Status                     | Meaning                                                                                                                                          |
    | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
    | `applied`                  | Phosra enforced the rule directly on the platform (e.g. curfew, monitoring).                                                                     |
    | `guided`                   | The platform accepts the setting but enforces it itself; Phosra configured and attested it (e.g. the console's own screen-time and rating gate). |
    | `unenforceable_categories` | Rules this platform has no surface for — a console can't run a web-content filter. They are reported, never silently dropped.                    |

    <Accordion title="Why 'guided' and not 'applied'?">
      A game console owns its own screen-time and content-rating controls. Phosra sets them and records a
      signed attestation (`enforcement_mode: manual_attested`) rather than pretending it flipped a switch
      it doesn't own. A DNS filter or MDM profile, by contrast, returns `applied` for the categories it
      enforces directly. The result report never conflates the two — you always know who is holding the
      line.
    </Accordion>
  </Step>
</Steps>

## The whole flow at a glance

| # | Step               | Call                                 | Live result                           |
| - | ------------------ | ------------------------------------ | ------------------------------------- |
| 1 | Find active policy | `GET /children/{id}/policies`        | policy `a3147895…`, `active`          |
| 2 | Locate the rule    | `GET /policies/{id}/rules`           | `time_daily_limit` = 180 min          |
| 3 | Tighten it         | `PUT /rules/{id}`                    | 120 min                               |
| 4 | Enforce            | `POST /children/{id}/enforce`        | job `67c763ff…`, `running`            |
| 5 | Poll               | `GET /enforcement/jobs/{id}`         | `completed` in \~0.14s                |
| 6 | Read results       | `GET /enforcement/jobs/{id}/results` | Xbox: 5 applied, 120-min limit guided |

## Next steps

<CardGroup cols={2}>
  <Card title="Onboard a multi-child household" icon="users" href="/guides/recipes/multi-child-household">
    Where the child and policy in this recipe came from — two kids, two age-tuned policies.
  </Card>

  <Card title="Enforcement, explained" icon="bolt" href="/concepts/enforcement">
    What `applied`, `guided`, and unenforceable mean per platform, and how attestation works.
  </Card>

  <Card title="Policies & rules" icon="list-check" href="/concepts/policies-and-rules">
    The full rule catalog and config shapes behind `time_daily_limit` and friends.
  </Card>

  <Card title="Retry a failed job" icon="rotate" href="/api-reference/enforcement/retry-enforcement-job">
    When a platform is offline mid-enforce, re-run just the failed results.
  </Card>
</CardGroup>
