Production access is free and self-serve with an account. · Synthetic demo · no account needed

FinchNode

Patient-authorized EHR integration

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, save its meta.changeCursor with the records. Then ask for changes since that cursor:

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