Picker Mode
Create a Connect Session where Medblocks hosts the EHR picker and completion UI.
Show prompt text
You are an AI coding agent integrating the Medblocks Platform API into the codebase that is currently open. Medblocks is a healthcare data platform; the Platform API is a server-to-server REST API that creates Patients, opens Connect Sessions (patient-mediated EHR/FHIR authorization), searches the FHIR source catalog, and reports connection status. This is a PHI-adjacent integration — do not guess your way through it.
Work in the five phases below. Do not skip ahead. When anything is ambiguous, STOP AND ASK rather than assume.
========================================
PHASE 0 — AUTHORITATIVE SOURCES
========================================
Fetch and read these before writing anything. If you cannot fetch a URL, ask the user to paste it. The OpenAPI spec is the only source of truth for routes, params, headers, request/response shapes, and error codes — it wins over anything in this prompt or in the docs if they ever disagree.
- OpenAPI spec (authoritative): https://app.medblocks.com/openapi/medblocks.json
- Setup + API keys + Version header: https://medblocks.com/docs/setup
- API overview + auth + fetch helper: https://medblocks.com/docs/api/overview
- Patients: https://medblocks.com/docs/api/patients
- Picker Mode (hosted picker): https://medblocks.com/docs/api/picker-mode
- Direct Mode (you pick the source): https://medblocks.com/docs/api/direct-mode
- After connection (return + status): https://medblocks.com/docs/api/after-connection
- FHIR access (roadmap): https://medblocks.com/docs/api/fhir-access
- Error envelope + code list: https://medblocks.com/docs/reference/errors
- Generated reference + playground: https://medblocks.com/docs/reference/api
From the OpenAPI spec, record before moving on:
- info.version (the date string used in the Version header; pin this for production).
- Every path under paths (the public surface today is /health, /patients, /patients/{id}, /patients/{id}/sessions, /sessions, /sessions/{id}, /connections, /connections/{id} — verify against the spec, not this prompt).
- components.securitySchemes (Bearer API key, format mb_sk_live_...).
- Callbacks defined on POST /sessions (pickerModeReturn, directModeSuccess, directModeError) — these describe what the patient browser receives on the return_url.
- The error envelope shape under any non-2xx response.
If a route or field you'd like to use is not in the spec, do not invent it. Say so and stop.
========================================
PHASE 1 — RECONNAISSANCE (analyze before writing)
========================================
Read the project before adding files. Determine, with evidence (cite the file you saw it in):
1. Language + runtime: look at package.json / bun.lock / pnpm-lock.yaml / yarn.lock / deno.json / requirements.txt / pyproject.toml / go.mod / Gemfile / Cargo.toml / pom.xml / build.gradle / *.csproj. Identify the runtime (Node, Bun, Deno, CPython, Ruby, Go, JVM, .NET, etc.) and exact framework versions.
2. HTTP framework: Next.js (App Router or Pages Router?), Remix, SvelteKit, Astro, Nuxt, Express, Hono, Fastify, Koa, NestJS, Bun.serve, FastAPI, Flask, Django (DRF?), Rails, Gin/Chi/Echo, Spring Boot, ASP.NET Core, Phoenix, etc.
3. Frontend layer (if any): React/Vue/Svelte/Solid? SSR or SPA? Build tool (Vite, Webpack, Bun, Turbopack)? Is there a frontend at all, or is this a backend-only service?
4. Existing API-client conventions: is there a services/, lib/, infra/, api/ folder? Are typed clients already generated from OpenAPI specs (openapi-typescript, openapi-fetch, orval, hey-api, swagger-codegen, openapi-python-client, oapi-codegen)? Is there a shared fetch wrapper or error class you should extend instead of duplicating?
5. Secrets + config: where are env vars loaded (.env, .env.local, dotenv, Bun's built-in loader, Pydantic Settings, Viper, dotenv-rails, Doppler, Vercel/Fly/Railway env)? Where is .env.example?
6. Auth + user model: does the app already have a "current user/patient" with a stable internal ID? That ID will become the Medblocks patient_id.
7. Logging + error handling: what logger (pino, winston, structlog, zerolog, slf4j, Python logging)? Is there a request-ID convention you should propagate?
8. Tests: which runner (bun test, vitest, jest, pytest, go test, rspec, junit)? Are HTTP calls mocked (msw, nock, responses, vcr)? Is there a sandbox or test API key already available?
9. Deployment target: Vercel / Cloudflare Workers / AWS Lambda / Fly / Railway / bare metal / on-prem? This affects timeout caps, edge-vs-node runtime, and where the backend route the browser calls actually runs.
Print a short reconnaissance report (5–8 bullets) before doing anything else. If you cannot identify any of items 1–6 with confidence, STOP and ask.
========================================
PHASE 2 — CLARIFYING QUESTIONS
========================================
Ask the user — by name, in a numbered list — every question below whose answer is not already obvious from Phase 1. Wait for answers. Do not stub things out and "fill in later".
A. Connect mode
- Picker Mode (Medblocks hosts the source picker), Direct Mode (your app picks the source and passes connection_id), or both?
- If Direct: where in the existing UI does the source-search experience belong?
B. Patient identity
- What is the stable, never-changing internal ID this app already has for a patient/user? It must not start with the Medblocks-reserved prefixes pat_, sess_, conn_, or fhirsrc_.
- Create Patients explicitly via POST /patients during intake, or upsert implicitly the first time a Session is created? (Upsert is the shorter path for most products.)
C. Return URL UX
- What absolute URL should Medblocks redirect the patient to after the hosted flow? (Usually a route this product owns, e.g. https://<app>/connected.)
- What product-specific label should the "Done" button show? ("Back to <ProductName>" beats "Continue".)
- On the return page, what should the patient see for success, failure, and cancellation?
D. Server boundary
- Confirm: this app has (or can add) a backend that holds MEDBLOCKS_API_KEY. The browser must never see it. Where in the codebase should the server-only Medblocks client live, given existing conventions?
E. Version pinning
- Pin the Version header to the spec's current info.version (today the spec reports 2026-04-25 — verify the value you just fetched) for production? Leave it off only during local exploration.
F. Idempotency + retries
- Is Session creation always user-initiated (one click → one Session) or also driven by a job that can retry? Medblocks does not currently document an Idempotency-Key header — re-check the spec before adding hand-rolled retry-with-jitter on writes.
G. Storage in this app's DB
- Should the app persist Medblocks session_id, connection_id, and last-known status in its own database, or treat Medblocks as the system of record and read on demand?
H. Eventing
- Webhooks are not documented in the current spec. Is the app fine with: (1) reading GET /sessions/{id} on return_url, and (2) reading GET /patients/{id} or GET /patients/{id}/sessions on demand for status?
I. Tests
- Unit only with mocked fetch, contract tests against medblocks.json, or live integration tests against a sandbox key?
J. Compliance + logging
- Any PHI redaction rules, BAA scope, or logging restrictions that affect what we can record (request bodies, patient names, emails)?
K. Anything else surprising in this codebase you want me to know before I touch it.
Where an answer is obvious from Phase 1 evidence, do not ask — state your assumption inline: "Assuming X because <file:line>; tell me if not."
========================================
PHASE 3 — PROPOSE A PLAN
========================================
Before generating code, post a short plan and pause for confirmation. The plan must include:
1. The exact files you will create or modify, with paths that follow THIS codebase's conventions (not generic Next.js paths if this isn't Next.js).
2. New env vars: MEDBLOCKS_API_KEY (required), MEDBLOCKS_API_URL (default https://app.medblocks.com), MEDBLOCKS_API_VERSION (the date from spec info.version). Where they will be documented (.env.example, README, deploy config).
3. Which endpoints you will call, by operationId from the spec. At minimum:
- api.createSession (POST /sessions) — Picker or Direct
- api.getSession (GET /sessions/{id}) — return_url verification
- api.createPatient (POST /patients) — only if explicit creation chosen
- api.getPatient (GET /patients/{id}) — only if patient-status UI needed
- api.listPatientSessions (GET /patients/{id}/sessions) — only if history UI needed
- api.listFhirSources (GET /connections) — only for Direct mode source search
4. Where the new UI hooks in: which existing component holds the "Connect" button, which page owns the return_url, and how status surfaces back to the user.
5. Error-mapping strategy: how Medblocks error.code + error.type values translate into this app's existing exception/HTTP shapes.
6. Test plan: what cases you'll cover and at what level (unit / contract / integration).
Wait for "go ahead" before writing code, unless the user explicitly told you to one-shot the implementation.
========================================
PHASE 4 — IMPLEMENTATION
========================================
Follow the conventions of THIS codebase. Do not impose Next.js patterns on Express, Express on FastAPI, etc. Generic rules:
1. ENV + CONFIG
- Add MEDBLOCKS_API_KEY to .env.example with a one-line comment about what it is and where to mint it. Update the project's env docs.
- NEVER expose the key through NEXT_PUBLIC_*, VITE_*, EXPO_PUBLIC_*, PUBLIC_*, or any other client-bundled prefix. NEVER import it from a module that ships to the browser. NEVER call the Medblocks API from a Server Component that streams to the client without first ensuring the key isn't serialized into the payload.
- Default MEDBLOCKS_API_URL to https://app.medblocks.com; allow override for sandboxes.
2. TYPED CLIENT
- If the project already has an OpenAPI codegen pipeline, extend it: feed medblocks.json through the same tool (openapi-typescript / orval / hey-api / openapi-python-client / oapi-codegen / quicktype / NSwag, whatever is in use).
- Otherwise, drop in one server-only module in the conventional location for this stack:
- Next.js (App Router): lib/medblocks.ts (or src/lib/medblocks.ts)
- Next.js (Pages): lib/medblocks.ts
- Remix / SvelteKit: app/lib/medblocks.server.ts (note the .server.ts suffix to lock it server-side)
- Express/Hono/NestJS: src/services/medblocks.ts
- Bun.serve project: src/medblocks.ts
- FastAPI: app/services/medblocks.py
- Django: <project>/services/medblocks.py
- Rails: app/services/medblocks.rb
- Go: internal/medblocks/client.go
- .NET: Services/MedblocksClient.cs
- …or wherever this repo already keeps third-party API clients.
- Every request must send:
- Authorization: Bearer ${MEDBLOCKS_API_KEY}
- Version: ${MEDBLOCKS_API_VERSION} (the date from spec info.version)
- Content-Type: application/json (on POST/PUT)
- Implement an error type that captures HTTP status + the full envelope: error.type, error.code, error.message, error.param, error.doc_url, error.request_id. Never throw raw Error("Medblocks failed") — keep the envelope.
- Use cursor pagination: list responses include has_more and next_cursor; pass starting_after on the next call.
3. SERVER ROUTES THE BROWSER CAN CALL
- One route per flow, all server-only. Route paths follow the framework's conventions. Each route forwards the upstream HTTP status and the Medblocks error body verbatim on failures, so the frontend can render error.message.
- Start Session (Picker): POST → calls api.createSession without connection_id. Body: { patient_id, return_url, return_button_label, patient_name?, patient_email?, recommended_connection_ids?, metadata? }. Response surfaces { id, url, status }.
- Start Session (Direct): POST → calls api.createSession WITH connection_id. Body: same as above but connection_id replaces recommended_connection_ids (the two are mutually exclusive — verify the spec).
- Read Session: GET ?id=... → calls api.getSession. Used by the return_url page.
- Search sources: GET ?q=... (Direct only) → calls api.listFhirSources with q + limit.
- (Optional) Patient: GET ?id=... → calls api.getPatient. Used for connected-source UI.
- (Optional) History: GET ?patient_id=... → calls api.listPatientSessions.
4. FRONTEND WIRING
- "Connect" button: POSTs to your server's start-session route, then sets window.location.href = body.url. Disable while pending.
- Return page: read session_id from the query string (always present). In Direct mode you also get success, connection_id, error, error_description. Treat the query string as a hint only. The source of truth is GET /sessions/{id}.connections — connections[].status === "active" means the patient is actually connected. session.status === "complete" only means the hosted flow finished.
- Render distinct UI for success / partial failure / outright failure / patient-cancelled. On every failure, log error.request_id so support can correlate.
5. BACKGROUND PULL — SET EXPECTATIONS EXPLICITLY
- A "complete" Session does NOT mean records are pulled. Medblocks schedules background retrieval after authorization. Surface "Connected — pulling records" to the user, then rely on dashboard exports or the upcoming FHIR access surface for record-level signals. Do not block the patient UI on data availability.
6. ERROR HANDLING
- Branch on error.code (stable) first, error.type (broad) only as a fallback. Common branches to handle by code:
- missing_api_key / invalid_api_key / expired_api_key → 500 to user, page on-call.
- unsupported_api_version → fail loudly in CI before deploy.
- bad_request / invalid_data → surface error.param to the caller with a 400.
- resource_not_found → 404 with a retry path.
- external_id_already_exists / resource_conflict → product decision on collision policy.
- throttled / quota_exceeded → exponential backoff with jitter; emit telemetry.
- 5xx api_error → bounded retry on idempotent GETs only; never silent-retry writes unless the spec adds Idempotency-Key.
- 502 ehr_error / oauth_error / token_exchange_failed / token_unavailable → surface to the user with a "reconnect" CTA; do NOT show raw portal text.
- Use the project's existing logger. Required log fields on every Medblocks failure: http_status, error.code, error.type, error.request_id, and the relevant patient_id / session_id.
7. IDEMPOTENCY + CONCURRENCY
- patient_id is the stable upsert key — re-sending POST /patients or upserting via POST /sessions with the same patient_id is safe. Do not invent surrogate IDs.
- Session creation is not idempotent: every call creates a new sess_*. If a background worker creates Sessions, persist the resulting session_id keyed by the originating intent BEFORE redirecting, to avoid duplicates.
8. SECURITY + PII
- No key in client bundles, ever. Re-check after bundling (grep the built output for the key prefix during CI).
- Do not log full patient names / emails unless the product's PII policy allows it. Always log session_id and request_id.
- All Medblocks traffic is over HTTPS. Verify the base URL is https://, not http://.
========================================
PHASE 5 — VERIFY (before reporting done)
========================================
Don't claim success until you've actually run things. Report results in this order:
1. Static checks pass: the project's typechecker (tsc --noEmit / mypy / pyright / go vet / cargo check / dotnet build), linter, and formatter.
2. Tests pass: existing suite + any new tests you wrote.
3. A real smoke test using a sandbox or dev API key:
a. Create or upsert a Patient.
b. Create a Picker (and/or Direct) Session — confirm the response has id (sess_*) and url.
c. Hit url in a browser, complete the hosted step in a sandbox EHR.
d. Land on your return_url. Confirm the page calls GET /sessions/{id} and renders connections[].status correctly.
4. Final report to the user:
- Files changed (paths).
- Env vars to set, where, and the spec's current info.version.
- Exact manual steps to repeat the smoke test.
- What you deliberately did NOT implement and why.
- Any assumption from Phase 2 that still needs human confirmation.
========================================
NON-NEGOTIABLES
========================================
- Server-only. API key never reaches a browser.
- Only call routes that exist in medblocks.json. Don't fabricate endpoints, fields, headers, or error codes.
- After every return_url, re-read GET /sessions/{id} from the server. The query string is a hint, not a source of truth.
- Version header must match the spec's info.version for production deploys.
- Log error.request_id on every failure.
- No retry-with-jitter on write endpoints without explicit spec support for idempotency.
- When uncertain, STOP and ask. In a healthcare integration, a wrong assumption baked in is worse than a slow integration.
========================================
DECISION HINTS
========================================
- Product already has a facility/EHR picker UX → Direct Mode.
- Product just wants a "Connect your records" button → Picker Mode.
- Both modes can coexist on the same API key.
- Stable patient_id ≠ email. Use an internal user ID, member ID, or chart ID — anything that won't change for that human.
- "Session complete" = patient finished the hosted flow. "Connection active" = Medblocks holds a usable token. Use connections[].status === "active" as the success signal, not session.status.Use Picker mode when you want Medblocks to host source search. Your app creates a Session, redirects the patient to Medblocks, and receives the patient back after the hosted flow finishes.
The patient can search the full catalog unless you pass recommended sources.

Create A Picker Session
Create a Session without connection_id. The server calls the Medblocks API. The client calls your server route and redirects the patient to the returned url.
async function startPickerFlow(patientId: string) {
const response = await fetch("/api/connect/picker", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
patientId,
returnUrl: `${window.location.origin}/connected`,
}),
});
const body = await response.json();
if (!response.ok) {
throw new Error(body.error?.message ?? "Could not start Connect");
}
window.location.href = body.url;
}patient_idstringrequiredStable patient ID from your system. Creates the Patient if needed.
patient_emailstringPatient email to store or update.
patient_namestringPatient display name to store or update.
recommended_connection_idsstring[]Source IDs to show first in the hosted picker. Mutually exclusive with connection_id.
return_urlstringrequiredURL to redirect after the Connect flow.
return_button_labelstringText shown on the patient-facing completion button.
metadataobjectAdditional metadata returned with the Session.
const MEDBLOCKS_BASE = process.env.MEDBLOCKS_API_URL ?? "https://app.medblocks.com";
const MEDBLOCKS_VERSION = "2026-04-25";
export async function createPickerSession(input: {
patientId: string;
patientName?: string;
patientEmail?: string;
returnUrl: string;
recommendedConnectionIds?: string[];
}) {
const response = await fetch(`${MEDBLOCKS_BASE}/sessions`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MEDBLOCKS_API_KEY}`,
"Content-Type": "application/json",
Version: MEDBLOCKS_VERSION,
},
body: JSON.stringify({
patient_id: input.patientId,
patient_name: input.patientName,
patient_email: input.patientEmail,
recommended_connection_ids: input.recommendedConnectionIds,
return_url: input.returnUrl,
return_button_label: "Back to your app",
}),
});
const body = await response.json();
if (!response.ok) throw new Error(body.error?.message ?? "Could not start Connect");
return {
id: body.id as string,
url: body.url as string,
status: body.status as string,
};
}patient_idstringrequiredStable patient ID from your system. Creates the Patient if needed.
patient_emailstringPatient email to store or update.
patient_namestringPatient display name to store or update.
recommended_connection_idsstring[]Source IDs to show first in the hosted picker. Mutually exclusive with connection_id.
return_urlstringrequiredURL to redirect after the Connect flow.
return_button_labelstringText shown on the patient-facing completion button.
metadataobjectAdditional metadata returned with the Session.
AuthorizationBearer <token>requiredMedblocks API key for server-side requests.
VersionstringDate-pinned API version. If omitted, Medblocks uses the version pinned on your API key.
Return Button
Picker mode ends on a Medblocks completion screen. At the bottom of that screen, Medblocks shows a button that sends the patient back to your app.
return_urlis the destination for that button.return_button_labelis the button text the patient sees.
Use product-specific wording. Back to TrialMatched, Continue, or Return to intake are better than a generic label.

When the patient clicks the button, Medblocks redirects to your return_url with the Session ID:
https://your-app.example.com/connected?session_id=sess_...session_idstringrequiredCompleted Session ID to read from your backend.
Picker mode does not include a success flag on the return URL. Read the Session and inspect connections.
Recommended Sources
Pass recommended_connection_ids when you already know likely sources but still want the patient to search if those are wrong. Recommended sources appear first in the hosted picker.

Omit recommended_connection_ids when the patient should search the full catalog.
Handle The Return
Do not trust the redirect by itself. Read the Session from your backend and use connections as the source of truth.
async function handlePickerReturn() {
const params = new URLSearchParams(window.location.search);
const sessionId = params.get("session_id");
if (!sessionId) return;
const response = await fetch(
`/api/connect/session?id=${encodeURIComponent(sessionId)}`,
);
const session = await response.json();
if (!response.ok) {
showRetry("We could not verify the connection. Please try again.");
return;
}
const active = session.connections.filter(
(connection: { status: string }) => connection.status === "active",
);
if (active.length > 0) {
showSuccess("Connected successfully");
} else {
showRetry("No sources were connected. Try again?");
}
}idstringrequiredSession ID from the return URL.
export async function readConnectSession(sessionId: string) {
const response = await fetch(
`${MEDBLOCKS_BASE}/sessions/${encodeURIComponent(sessionId)}`,
{
headers: {
Authorization: `Bearer ${process.env.MEDBLOCKS_API_KEY}`,
Version: MEDBLOCKS_VERSION,
},
},
);
const body = await response.json();
if (!response.ok) throw new Error(body.error?.message ?? "Could not read session");
return body as {
id: string;
status: string;
connections: Array<{ connection_id: string; status: string }>;
};
}idstringrequiredSession ID from the return URL.
AuthorizationBearer <token>requiredMedblocks API key for server-side requests.
VersionstringDate-pinned API version. If omitted, Medblocks uses the version pinned on your API key.
Common Errors
| Code | Meaning |
|---|---|
invalid_api_key | The API key is missing, invalid, or expired. |
unsupported_api_version | The Version header is not supported. |
resource_not_found | A recommended source ID does not exist or is not visible to your organization. |
bad_request | Request fields are missing, malformed, or mutually exclusive. |
Read the full envelope and code list in API Errors.
Related Articles
- Build your own source picker with Direct Mode.
- Read the concept page: Session.
- Continue with After Connection.
- Inspect the schema in Create a Connect Session.
