# Change feed

> After your first import, read only what changed in a category, including deletions.

## Start from your first import

When you [import a category](/docs/records/read-and-paginate), save its `meta.changeCursor` with the records. Then ask for changes since that cursor:

```bash
curl "https://api.finchnode.com/api/v1/users/$SUBJECT/records/labs/changes?cursor=$CHANGE_CURSOR&limit=100" \
  -H "Authorization: Bearer $FINCHNODE_API_KEY"
```

## Apply changes in order

Each change has a `sequence`. Apply them in ascending order.

| `changeType` | What to do |
| --- | --- |
| `upsert` | Insert or replace the record with this `recordId`, using `record`. |
| `delete` | Remove the record with this `recordId`. `record` is `null`. |

`record` is the record as it is now, not as it was at that sequence. An older `upsert` can have `record: null` when the record was deleted later. Keep going: the later `delete` settles it.

## Stop on hasMore

On this endpoint `nextCursor` is never `null`. Stop when `hasMore` is `false`, and save that last `nextCursor` for next time.

Node.js:

```js
async function applyChanges(subject, category, cursor, db) {
  for (;;) {
    const url = new URL(`https://api.finchnode.com/api/v1/users/${subject}/records/${category}/changes`);
    url.searchParams.set('cursor', cursor);
    url.searchParams.set('limit', '100');
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.FINCHNODE_API_KEY}` } });
    if (!res.ok) throw new Error(`FinchNode returned ${res.status}`);
    const page = await res.json();
    await db.transaction(async (tx) => {
      for (const change of page.data) {
        if (change.changeType === 'delete' || !change.record) await tx.deleteRecord(change.recordId);
        else await tx.upsertRecord(change.recordId, change.record);
      }
      await tx.saveChangeCursor(subject, category, page.nextCursor);
    });
    cursor = page.nextCursor;
    if (!page.hasMore) return cursor;
  }
}
```
Python:

```python
def apply_changes(subject, category, cursor, db):
    while True:
        res = requests.get(
            f"https://api.finchnode.com/api/v1/users/{subject}/records/{category}/changes",
            headers={"Authorization": f"Bearer {os.environ['FINCHNODE_API_KEY']}"},
            params={"cursor": cursor, "limit": 100},
        )
        if not res.ok:
            raise RuntimeError(f"FinchNode returned {res.status_code}")
        page = res.json()
        with db.transaction() as tx:
            for change in page["data"]:
                if change["changeType"] == "delete" or change["record"] is None:
                    tx.delete_record(change["recordId"])
                else:
                    tx.upsert_record(change["recordId"], change["record"])
            tx.save_change_cursor(subject, category, page["nextCursor"])
        cursor = page["nextCursor"]
        if not page["hasMore"]:
            return cursor
```

`db` is your own database. Saving the cursor in the same transaction as the changes means a crash resumes from the last saved point, without skipping or repeating changes.

## When to read it

- On `records.updated`, which arrives after a continuous sync changed or deleted records. Its `data.categories` lists the shared categories; read each one's feed.
- On a schedule, for one-time shares or if you don't use webhooks.

## What it won't tell you

- A record leaves the feed as `delete` only when the health system's search for it succeeded and the record was gone. A failed search never turns into deletions.
- If the patient revokes one health system and still shares another, that system's records drop out of reads with no `delete` entries. Remove them yourself. See [Consent, revocation, and deletion](/docs/records/consent-revocation-deletion).
