PatientFlow

mb.patientFlow.init and retrieve. The primary Connect integration primitive.

A PatientFlow is one patient’s pass through the picker, OAuth login, token exchange, and return-to-app sequence. The SDK exposes two methods on mb.patientFlow:

MethodHTTP
mb.patientFlow.init(input)POST /patient-flows
mb.patientFlow.retrieve(id)GET /patient-flows/{id}

Pick picker mode when you want Medblocks to host the EHR search UI; pick direct mode when your app already knows which EHR the patient is connecting. Both modes use the same init call. The request shape selects the mode.

Picker Mode

Omit connection_id. Optionally pass recommended_connection_ids to push specific EHRs to the top of the picker.

server/routes/start-picker.ts
import { mb } from "../medblocks";

export async function startPicker(patientId: string, patientEmail: string) {
  const flow = await mb.patientFlow.init({
    patient_id: patientId,
    patient_email: patientEmail,
    return_url: "https://app.example.com/connected",
    return_button_label: "Back to Acme Health",
    recommended_connection_ids: ["fhirsrc_epic_mychart"],
  });
  return { url: flow.url };
}
connect-button.ts
async function startConnect(patientId: string, patientEmail: string) {
  const response = await fetch("/api/start-picker", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ patientId, patientEmail }),
  });
  const { url } = await response.json();
  window.location.href = url;
}

After the patient clicks Done on the Medblocks completion screen, they land on your return_url with ?patient_id=<your-external-id>&patient_flow_id=pf_* appended. Use patient_id with mb.patients.retrieve for the patient’s cumulative state; use patient_flow_id with mb.patientFlow.retrieve for this session specifically.

Direct Mode

Pass a single connection_id returned from mb.connections.list. The Medblocks-hosted page redirects to that EHR’s SMART login in about 200 ms. No Medblocks UI is rendered.

server/routes/start-direct.ts
import { mb } from "../medblocks";

export async function startDirect(input: {
  patientId: string;
  connectionId: string;
}) {
  const flow = await mb.patientFlow.init({
    patient_id: input.patientId,
    connection_id: input.connectionId,
    return_url: "https://app.example.com/connected",
  });
  return { url: flow.url };
}
connect-direct.ts
async function connectFacility(patientId: string, connectionId: string) {
  const response = await fetch("/api/start-direct", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ patientId, connectionId }),
  });
  const { url } = await response.json();
  window.location.href = url;
}

The return URL carries patient_id, patient_flow_id, success, connection_id, and on failure error / error_description. Use parseReturnUrl to read these in a typed way.

connection_id accepts either the fhirsrc_* public id or the raw fhir_base_url.

Retrieve

Read the PatientFlow from the server after the patient returns. The connections array is the source of truth for what actually got authorized.

server/routes/connect-status.ts
import { mb } from "../medblocks";

export async function connectStatus(id: string) {
  const flow = await mb.patientFlow.retrieve(id);
  return {
    status: flow.status,
    active: flow.connections.filter((c) => c.status === "active"),
    failed: flow.connections.filter((c) => c.status === "failed"),
  };
}

flow.status is the lifecycle of the browser flow itself. open, complete, expired. flow.connections[*].status describes each authorization attempt. active, failed, expired, refresh_failed.

Input Reference

InitPatientFlowInput is the type of mb.patientFlow.init’s argument. The schema lives in Initialize a Connect PatientFlow.

FieldRequiredNotes
patient_idYesYour stable patient identifier. Reserved prefixes (pat_, pf_, conn_, fhirsrc_, wh_, evt_) are rejected.
return_urlYesAbsolute URL the patient lands on after the flow.
patient_emailNoStored or updated on the patient.
patient_nameNoStored or updated on the patient.
connection_idNoEHR id for direct mode. Mutually exclusive with recommended_connection_ids.
recommended_connection_idsNoEHR ids surfaced first in picker mode. Mutually exclusive with connection_id.
return_button_labelNoCustom label for the “Done. Return to {label}” button. Max 60 chars.
expires_inNoSeconds until the URL expires. Default 1800 (30 min). Max 86400 (24 h).
metadataNoArbitrary key/value pairs echoed back on reads.

If both connection_id and recommended_connection_ids are present, the SDK forwards the request and the API returns MedblocksInvalidRequestError.

Errors

SubclassWhen
MedblocksInvalidRequestErrorconnection_id and recommended_connection_ids both set; return_url malformed; reserved-prefix patient_id.
MedblocksConflictErrorpatient_id already exists in a way that conflicts with the supplied email/name.
MedblocksNotFoundErrorconnection_id does not exist or is not visible to your organization.
MedblocksRateLimitErrorQuota exceeded. Honors Retry-After.

See Errors for the full hierarchy.