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.
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;
}
}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 cursordb 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. Itsdata.categorieslists 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
deleteonly 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
deleteentries. Remove them yourself. See Consent, revocation, and deletion.