Compliance API

Endpoint — changes

The change feed. Integrators who want to stay in sync without polling all 51 states should hit this endpoint on a cron and process only what's new.

GET /api/v1/compliance/changes

Returns versioned change events newest-first, with cursor-based pagination.

curl -H "Authorization: Bearer $TOKEN" \
  "https://homeschoolfox.com/api/v1/compliance/changes?since=2026-05-01T00:00:00Z&limit=100"
{
  "data": [
    {
      "resource_type": "State",
      "resource_key": "MO",
      "version": 2,
      "released_at": "2026-05-12",
      "git_sha": "ec3bc5a2",
      "committed_at": "2026-05-12T20:40:02-05:00",
      "committed_by": "Andy Leverenz",
      "change_summary": "MO: bump required hours after legislative update",
      "source": "human",
      "diff": {
        "added_keys": [],
        "removed_keys": [],
        "changed_keys": ["requirements"]
      }
    },
    ...
  ],
  "meta": {
    "api_version": "v1",
    "count": 100,
    "next_cursor": "eyJjb21taXR0ZWRfYXQiOiIyM..."
  }
}

Query params

Param Required Notes
since no ISO 8601 timestamp. Filters to events with committed_at >= since. Use the previous run's max committed_at.
resource_type no One of State, EsaProgram, Citation. Filters to that type.
cursor no Opaque pagination cursor returned in meta.next_cursor. Echo it back to fetch the next page.
limit no 1–500, default 100.

Pagination

When more results exist, the response includes meta.next_cursor. Pass it as ?cursor=... to fetch the next page. When there are no more results, next_cursor is omitted.

# First page
curl ".../changes?since=2026-01-01"
# → returns 100 results + next_cursor

# Next page
curl ".../changes?since=2026-01-01&cursor=eyJjb21taXR0ZWRfYXQiOiIyM..."

source field

Tracks how each change was made:

  • human — a person committed the YAML edit (the only source in v1)
  • llm_drafted — an LLM drafted a PR that a human merged (Phase 7 roadmap)
  • automated_sync — committed directly by a sync job (Phase 8+ roadmap)

You can use this to filter or display provenance differently in your UI.

last_seen = read_from_local_state()  # ISO 8601 timestamp
cursor = None

while True:
    params = {"since": last_seen}
    if cursor: params["cursor"] = cursor

    res = http.get(API + "/changes", params=params, auth=BEARER)
    for change in res["data"]:
        apply_change(change)
        last_seen = max(last_seen, change["committed_at"])

    cursor = res["meta"].get("next_cursor")
    if not cursor:
        break

save_to_local_state(last_seen)