Quickstart
Send a test patient to sign in at a sandbox hospital and read the records they share.
Medblocks lets a patient connect their own hospital records to your app. In this quickstart you’ll run a small server, send a test patient to sign in at a sandbox hospital, and read the records they share.
How it works
Four things happen.
- Your server starts the connection and redirects the patient to the URL Medblocks returns.
- The patient signs in to their hospital and approves access to their records.
- They come back to your app, and you read which hospitals they connected.
- Medblocks pulls the approved records in the background, and you read them with one call.
That is the whole flow. Let’s see how we can build it.
Install
npm install medblockspnpm add medblocksyarn add medblocksbun add medblocksAdd your API key
Your API key is a secret, so it lives only on your server. Put it in an environment variable. You can create one in your workspace.
MEDBLOCKS_API_KEY="mb_sk_live_..."Create the client
Create the client once and reuse it everywhere. It reads the key from the environment and fails fast if it is missing. This file is the same whichever server you use.
import { Medblocks } from "medblocks";
const apiKey = process.env.MEDBLOCKS_API_KEY;
if (!apiKey) throw new Error("MEDBLOCKS_API_KEY is not configured");
export const mb = new Medblocks(apiKey);Start the connection
Add a route the patient opens to begin. It starts the connection and redirects the browser to the URL Medblocks returns. return_url is where the patient lands when they finish.
import { mb } from "./medblocks";
/**
* GET /start — open a patient session and redirect the patient's browser to the
* Medblocks-hosted page. No frontend: the redirect is the whole integration.
*/
export async function initPatientSession(_req: Request): Promise<Response> {
const session = await mb.patientSession.init({
patient_id: "demo-patient",
return_url: "http://localhost:3000/connected",
return_button_label: "Back to Acme Health",
});
return Response.redirect(session.url, 302);
}import type { Request, Response } from "express";
import { mb } from "./medblocks";
/**
* GET /start — open a patient session and redirect the patient to the
* Medblocks-hosted page. No frontend: the redirect is the whole integration.
*/
export async function initPatientSession(_req: Request, res: Response) {
const session = await mb.patientSession.init({
patient_id: "demo-patient",
return_url: "http://localhost:3000/connected",
return_button_label: "Back to Acme Health",
});
res.redirect(session.url);
}Read the connected facilities
When the patient finishes, they land on your return_url. Read who came back, look up the facilities they connected, and show them.
import { parseReturnUrl } from "medblocks";
import { mb } from "./medblocks";
/**
* GET /connected — the return_url. The patient lands here after authorizing.
* Read who returned, look up the sources they connected, and say so.
*/
export async function connected(req: Request): Promise<Response> {
const result = parseReturnUrl(new URL(req.url).searchParams);
if (!result) return new Response("Not a return URL", { status: 400 });
const connections = await mb.patients.getConnections(result.patient_id, {
hydrate: true,
});
const facilities = connections.map((c) => c.name).join(", ");
return new Response(`Connected to ${facilities || "your records"} successfully`);
}import type { Request, Response } from "express";
import { parseReturnUrl } from "medblocks";
import { mb } from "./medblocks";
/**
* GET /connected — the return_url. Read who returned and which sources they connected.
*/
export async function connected(req: Request, res: Response) {
const result = parseReturnUrl(new URL(req.url, "http://localhost:3000").searchParams);
if (!result) {
res.status(400).send("Not a return URL");
return;
}
const connections = await mb.patients.getConnections(result.patient_id, {
hydrate: true,
});
const facilities = connections.map((c) => c.name).join(", ");
res.send(`Connected to ${facilities || "your records"} successfully`);
}Read the records
Connecting is only half the story. The point is the data. Once a patient approves access, Medblocks pulls their records from the hospital in the background, and one call returns everything it has stored, as raw FHIR resources.
Add a route that reads them with mb.patients.records. Under the hood the SDK calls GET /patients/{id}/records.
import { mb } from "./medblocks";
/**
* GET /records?patient_id=... — read the patient's connected records on demand.
* Returns the first page of the latest stored FHIR resources as one line per resource.
*/
export async function patientRecords(req: Request): Promise<Response> {
const patientId = new URL(req.url).searchParams.get("patient_id");
if (!patientId) return new Response("Missing patient_id", { status: 400 });
const page = await mb.patients.records(patientId, { count: 100 });
const lines = page.data.map(
(item) => `${item.resource.resourceType}/${item.resource.id}`,
);
return new Response(`Found ${lines.length} resources\n${lines.join("\n")}`);
}import type { Request, Response } from "express";
import { mb } from "./medblocks";
/**
* GET /records?patient_id=... — read the patient's connected records on demand.
* Returns the first page of the latest stored FHIR resources as one line per resource.
*/
export async function patientRecords(req: Request, res: Response) {
const patientId = typeof req.query.patient_id === "string" ? req.query.patient_id : "";
if (!patientId) {
res.status(400).send("Missing patient_id");
return;
}
const page = await mb.patients.records(patientId, { count: 100 });
const lines = page.data.map(
(item) => `${item.resource.resourceType}/${item.resource.id}`,
);
res.type("text/plain").send(`Found ${lines.length} resources\n${lines.join("\n")}`);
}Run it
Put your three routes behind a server on port 3000.
import { initPatientSession } from "./init-patient-session";
import { connected } from "./connections";
import { patientRecords } from "./patient-records";
Bun.serve({
port: 3000,
routes: {
"/start": { GET: initPatientSession },
"/connected": { GET: connected },
"/records": { GET: patientRecords },
},
});
console.log("open http://localhost:3000/start");import express from "express";
import { initPatientSession } from "./init-patient-session";
import { connected } from "./connections";
import { patientRecords } from "./patient-records";
const app = express();
app.get("/start", initPatientSession);
app.get("/connected", connected);
app.get("/records", patientRecords);
app.listen(3000, () => console.log("open http://localhost:3000/start"));Then start it.
bun run src/server.tsUsing Node? Run node --env-file=.env --import tsx src/server.ts instead.
Connect the Epic sandbox
Open http://localhost:3000/start. Search for Epic Sandbox, sign in with the test credentials, and approve access.
They return to your app, where the /connected route reads what they connected and shows it.
See their records
Approving grants access. The records themselves land a moment later through the background pull. Open http://localhost:3000/records?patient_id=demo-patient to see what has arrived.
Found 58 resources
Patient/eq081-VQEgP8drUUqCWzHfw3
Observation/ejPFhUXYm7Bnv7yiCQnKsA3
Condition/e-Y7RyVDLDdLdRhU5N35rBQ3
...If the count is still zero, the pull has not settled yet. Wait a little and refresh. Each item is a raw FHIR resource exactly as the hospital returned it, paired with the source it came from, and the response is a paginated list. Get the data covers pagination, filtering by resource type, and reading only what changed since your last read.
That’s it 🎉
A patient connected their hospital, and you read their clinical records. That is a full patient integration, from connection to data, with no per-hospital code on your side.
What just happened
You just connected a patient to their hospital and read their records, end to end, without writing any hospital-specific code.
- Medblocks hosted the page where the patient picked their source. To build that screen in your own app, see your own UI.
- The records keep arriving after the patient returns. Later pulls add whatever is new, so
/recordsreturns more over time. Get the data covers the read API in full, plus exports that deliver records to your own systems. - For the full picture, the build guide walks through a complete integration, server and client.
