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.
Recommended sync pattern
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)