Patient-authorized EHR integration
Quickstart
Get a synthetic patient's records into your terminal with a sandbox key, no hospital sign-in needed.
You'll create a Connect session, let the sandbox complete it with a synthetic patient, and read that patient's medications. Every record here is synthetic.
Before you start
- A FinchNode developer account. Sign up if you don't have one.
- An application in the console under Applications, with
medicationsandlabsamong its data categories. - A sandbox key for it, starting with
ck_test_. The console shows it once, so copy it then. curl, Node.js 18 or later, or Python 3 withrequests(pip install requests).
Put the key in your shell, not in code or URLs:
export FINCHNODE_API_KEY='PASTE_YOUR_CK_TEST_KEY'
Read a synthetic patient's records
Create a Connect session. Both fields are optional;
categoriesmust be on your application's list.cURL curl -X POST "https://api.finchnode.com/api/v1/connect/sessions" \ -H "Authorization: Bearer $FINCHNODE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"externalId": "user_123", "categories": ["medications", "labs"]}'Node.js const response = await fetch('https://api.finchnode.com/api/v1/connect/sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FINCHNODE_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ externalId: 'user_123', categories: ['medications', 'labs'] }), }); if (!response.ok) throw new Error(`FinchNode returned ${response.status}`); const session = await response.json();Python import os import requests response = requests.post( "https://api.finchnode.com/api/v1/connect/sessions", headers={"Authorization": f"Bearer {os.environ['FINCHNODE_API_KEY']}"}, json={"externalId": "user_123", "categories": ["medications", "labs"]}, ) response.raise_for_status() session = response.json()You get a
201with the session (abbreviated here). Its link is for one patient and expires in 24 hours.{ "id": "cs_4b2e9d0c7a1f5e3d8c6b", "object": "connect_session", "status": "pending", "categories": ["medications", "labs"], "subject": null, "environment": "sandbox", "expiresAt": "2026-10-01T18:00:00.000Z", "url": "https://finchnode.com/connect/cs_4b2e9d0c7a1f5e3d8c6b" }In a shell, save the ID with
export SESSION_ID='PASTE_THE_ID'.Complete it with a synthetic patient. In the sandbox you can skip the patient's browser;
baseline-adultis a fictional adult with diabetes and hypertension.cURL curl -X POST "https://api.finchnode.com/api/v1/connect/sessions/$SESSION_ID/simulate" \ -H "Authorization: Bearer $FINCHNODE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"scenario": "baseline-adult"}'Node.js const simulated = await fetch(`https://api.finchnode.com/api/v1/connect/sessions/${session.id}/simulate`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.FINCHNODE_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ scenario: 'baseline-adult' }), }); if (simulated.status !== 202) throw new Error(`FinchNode returned ${simulated.status}`);Python simulated = requests.post( f"https://api.finchnode.com/api/v1/connect/sessions/{session['id']}/simulate", headers={"Authorization": f"Bearer {os.environ['FINCHNODE_API_KEY']}"}, json={"scenario": "baseline-adult"}, ) simulated.raise_for_status()You get a
202, and the simulation runs in the background.Wait for it to finish. Poll until
simulation.stateiscompleted, and stop if it'sfailed.cURL curl "https://api.finchnode.com/api/v1/connect/sessions/$SESSION_ID" \ -H "Authorization: Bearer $FINCHNODE_API_KEY"Node.js let current; for (let attempt = 0; attempt < 60; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 2000)); const res = await fetch(`https://api.finchnode.com/api/v1/connect/sessions/${session.id}`, { headers: { Authorization: `Bearer ${process.env.FINCHNODE_API_KEY}` }, }); if (!res.ok) throw new Error(`FinchNode returned ${res.status}`); current = await res.json(); if (['completed', 'failed'].includes(current.simulation.state)) break; } if (current.simulation.state !== 'completed') { throw new Error(`Simulation did not complete: ${current.simulation.failureCode ?? 'timed out'}`); }Python import time for attempt in range(60): time.sleep(2) polled = requests.get( f"https://api.finchnode.com/api/v1/connect/sessions/{session['id']}", headers={"Authorization": f"Bearer {os.environ['FINCHNODE_API_KEY']}"}, ) if not polled.ok: raise RuntimeError(f"FinchNode returned {polled.status_code}") current = polled.json() if current["simulation"]["state"] in ("completed", "failed"): break if current["simulation"]["state"] != "completed": raise RuntimeError(f"Simulation did not complete: {current['simulation'].get('failureCode', 'timed out')}")When it completes,
subjectholds the patient's ID for your app. In a shell,export SUBJECT='PASTE_THE_SUBJECT'.{ "id": "cs_4b2e9d0c7a1f5e3d8c6b", "status": "completed", "subject": "u_4f3a9c1e2b7d6a05", "simulation": { "scenario": "baseline-adult", "state": "completed" } }Read the records. Use the
subjectfrom this session, never one looked up fromGET /users.cURL curl "https://api.finchnode.com/api/v1/users/$SUBJECT/records/medications?limit=25" \ -H "Authorization: Bearer $FINCHNODE_API_KEY"Node.js const recordResponse = await fetch( `https://api.finchnode.com/api/v1/users/${current.subject}/records/medications?limit=25`, { headers: { Authorization: `Bearer ${process.env.FINCHNODE_API_KEY}` } }, ); if (!recordResponse.ok) throw new Error(`FinchNode returned ${recordResponse.status}`); const records = await recordResponse.json();Python record_response = requests.get( f"https://api.finchnode.com/api/v1/users/{current['subject']}/records/medications", headers={"Authorization": f"Bearer {os.environ['FINCHNODE_API_KEY']}"}, params={"limit": 25}, ) if not record_response.ok: raise RuntimeError(f"FinchNode returned {record_response.status_code}") records = record_response.json()You get one page of records in FinchNode's normalized format (abbreviated, synthetic):
{ "object": "list", "category": "medications", "data": [ { "id": "rec_a81e05c4c049bf549c1e4fc5", "resourceType": "MedicationRequest", "name": "Metformin 500 mg tablet", "status": "active", "startDate": "2026-07-18", "source": "northstar-health", "sourceName": "Northstar Health System (Synthetic)", "codes": [{ "system": "http://www.nlm.nih.gov/research/umls/rxnorm", "code": "861007", "display": "Metformin 500 MG Oral Tablet" }] } ], "hasMore": false, "nextCursor": null }
Why poll simulation.state and not status? For multi-source-overlap, source-unavailable, and consent-revoked, status reads completed before the simulation finishes.
Sandbox limits
An application holds up to 25 active synthetic patients by default. Each becomes eligible for purge 7 days after it's created. Past the limit, simulate returns 409 sandbox_limit_reached.
Connect through the browser instead
Open the session's url instead of calling simulate. With a sandbox key, the health-system search starts with a list of synthetic health systems. Pick a scenario's source, such as Northstar Health System (Synthetic), and finish the two consent screens. No hospital sign-in is needed.
You'll also go through the patient's account step. If you aren't already signed in to a FinchNode account in that browser, it asks you to create one and verify the email.
What's next
- After the patient connects: tie a finished session to your own user.
- Record model and categories: every field and category.
- Scenarios: try failures, partial consent, and revoked access.