shteg.ai speaks FHIR R4. The clinical layer exposes US Core resources, the payment layer carries the money, and the two meet at a single interoperability surface: read/search, Bulk Data $export, SMART App Launch context, CDS Hooks, C-CDA export, and a patient-mediated Blue Button import. Nothing here fabricates a resource, a job, or a store.
GET /api/fhir/metadata — it advertises exactly the resource types and interactions actually exposed by your deployment. Where this prose and the CapabilityStatement disagree, the CapabilityStatement wins.The exposed surface is predominantly read and search. Bulk Data $export and FHIR-store sync both fail closed — with no FHIR store configured they return a typed 501 with zero network calls, never a fabricated job or manifest. This is the sandbox surface — real logic, zero live data. No live money or live patient data is reachable from this tier; production access is a separate approval.
Every request is JSON (or FHIR+JSON) over HTTPS against your sandbox origin. Every API request is authenticated before it reaches a handler; FHIR reads are staff-only and scope-checked. Public discovery routes are the deliberate exception.
/api/fhir/metadata and /api/fhir/cds-services are intentionally public: a CapabilityStatement and a CDS Hooks catalogue are discovery documents. They carry no PHI.
Resource read/search and export kickoff require a staff session with patients:read . Requests are automatically scoped to your organization from your credentials — you never pass an organization or clinic identifier, and one organization can never read another’s resources.
The Patient Access API answers under an authorized patient session; SMART launch context resolves from the access token itself. The token is the context — patient, encounter, scope, and fhirUser.
An unconfigured backing FHIR store returns a typed 501, never a simulated bundle. A missing or unauthorized resource returns 404 / 403 — never a fabricated one.
The FHIR R4 CapabilityStatement for your deployment — the authoritative list of resource types and interactions actually exposed. Read this first, and re-read it rather than trusting a hardcoded assumption about write support.
Auth Public
| Field | Type | Description |
|---|---|---|
resourceType | string | Always "CapabilityStatement". |
fhirVersion | string | The FHIR version — "4.0.1" (R4). |
rest | array | REST capabilities: the resource types exposed and the interactions (read/search/etc.) supported for each. |
rest[].resource[].interaction | array | The exact interactions per resource. Trust this over any prose — the exposed surface is predominantly read + search. |
# Public — no auth. The source of truth for the exposed surface.
curl "https://your-sandbox-origin.example/api/fhir/metadata" \
-H "Accept: application/fhir+json"const res = await fetch("/api/fhir/metadata", {
headers: { Accept: "application/fhir+json" },
});
const capability = await res.json();
// Enumerate what is actually exposed:
const exposed = capability.rest?.[0]?.resource?.map((r) => ({
type: r.type,
interactions: r.interaction?.map((i) => i.code),
}));{
"resourceType": "CapabilityStatement",
"status": "active",
"fhirVersion": "4.0.1",
"format": ["application/fhir+json"],
"rest": [
{
"mode": "server",
"resource": [
{
"type": "Patient",
"interaction": [{ "code": "read" }, { "code": "search-type" }]
},
{
"type": "Observation",
"interaction": [{ "code": "read" }, { "code": "search-type" }]
}
]
}
]
}The FHIR R4 resource surface. It is predominantly a GET / search surface — read the CapabilityStatement for the exact interactions supported per resource type. Do not assume write support that metadata does not advertise.
Auth Staff session · scope patients:read
| Name | Type | In | Required | Description |
|---|---|---|---|---|
Accept | string | header | Optional | application/fhir+json is preferred. Defaults to FHIR+JSON. |
resourceType | string | path | Optional | The FHIR resource type to read or search (e.g. Patient, Observation). Must be advertised in the CapabilityStatement. |
<search params> | string | header | Optional | Standard FHIR R4 search parameters as query string (e.g. patient, category, date). Only parameters the CapabilityStatement advertises are honored. |
# Search Observations for a patient (staff session)
curl "https://your-sandbox-origin.example/api/fhir/Observation?patient=Patient/pat_9f2a41&category=vital-signs" \
-H "Accept: application/fhir+json" \
-H "Cookie: $SHTEG_SESSION"const params = new URLSearchParams({
patient: "Patient/pat_9f2a41",
category: "vital-signs",
});
const res = await fetch(`/api/fhir/Observation?${params}`, {
headers: { Accept: "application/fhir+json" },
credentials: "include",
});
const bundle = await res.json(); // FHIR searchset Bundle| Status | Meaning |
|---|---|
401 | Request is not authenticated. |
403 | Session lacks patients:read, or the resource is outside your organization. |
404 | No such resource in your organization — never a fabricated one. |
501 | Fail-closed: the requested capability is not exposed / the backing store is unconfigured; nothing is synthesized. |
Kick off a FHIR Bulk Data ($export) job at Patient scope or Group scope. Both return a signed ExportJob you then poll. Both fail closed with 501 when no FHIR store is configured — no job is created and no manifest is fabricated.
Auth Staff session
signed job token, then you poll /api/fhir/bulk-status/[jobId] until the manifest is ready. The kickoff itself moves no data. Group scope requires the Group id; Patient scope exports your organization’s patient compartment.| Name | Type | In | Required | Description |
|---|---|---|---|---|
id | string | path | Required | Group-scope only: the FHIR Group id whose members are exported. |
_type | string | header | Optional | Comma-separated FHIR resource types to include (standard Bulk Data param). Defaults to the exposed US Core set. |
_since | string | header | Optional | FHIR instant — restrict to resources changed since this timestamp. |
# Kick off a Patient-scope bulk export. Returns a signed job token.
curl "https://your-sandbox-origin.example/api/fhir/Patient/\$export?_type=Patient,Observation" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
-H "Cookie: $SHTEG_SESSION"const res = await fetch(
`/api/fhir/Group/${groupId}/export?_type=Patient,Observation`,
{
headers: { Accept: "application/fhir+json", Prefer: "respond-async" },
credentials: "include",
},
);
// 501 if no FHIR store is configured — no job created.
const { jobId, statusUrl } = await res.json();{
"jobId": "exp_3b7c19",
"scope": "Patient",
"statusUrl": "/api/fhir/bulk-status/exp_3b7c19",
"acceptedAt": "2026-07-14T18:20:03.512Z"
}| Status | Meaning |
|---|---|
401 | No valid staff session. |
403 | Session lacks the read scope, or the Group is outside your organization. |
404 | Group scope: no such Group in your organization. |
501 | Fail-closed: no backing FHIR store configured — no export job is created and no manifest is fabricated. |
The async polling target for a running $export. While the job runs it reports progress; when complete it returns the Bulk Data manifest of NDJSON output files, which you then download. The signed job token is the credential.
Auth Signed job token
| Name | Type | In | Required | Description |
|---|---|---|---|---|
jobId | string | path | Required | The ExportJob id returned by the $export kickoff. |
| Field | Type | Description |
|---|---|---|
transactionTime | string | FHIR instant marking the export snapshot (present on the completed manifest). |
output | array | The manifest: one entry per NDJSON output file, each with a FHIR type and a download URL. |
error | array | Any OperationOutcome files produced by the export. |
# Poll the signed status URL until 200 with a manifest
curl "https://your-sandbox-origin.example/api/fhir/bulk-status/exp_3b7c19" \
-H "Authorization: Bearer $EXPORT_JOB_TOKEN"
# 202 = still running (Retry-After header). 200 = manifest ready.async function pollExport(statusUrl, token) {
for (;;) {
const res = await fetch(statusUrl, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 200) return res.json(); // manifest
if (res.status !== 202) throw new Error(`bulk-status ${res.status}`);
const wait = Number(res.headers.get("Retry-After") ?? 5) * 1000;
await new Promise((r) => setTimeout(r, wait));
}
}{
"transactionTime": "2026-07-14T18:20:03.512Z",
"request": "/api/fhir/Patient/$export",
"requiresAccessToken": true,
"output": [
{ "type": "Patient", "url": "/api/fhir/bulk-status/exp_3b7c19/Patient.ndjson" },
{ "type": "Observation", "url": "/api/fhir/bulk-status/exp_3b7c19/Observation.ndjson" }
],
"error": []
}| Status | Meaning |
|---|---|
401 | Missing or invalid signed job token. |
403 | The token does not authorize this job. |
404 | No such job — expired or never created. |
501 | Fail-closed: no FHIR store backs the job; nothing is synthesized. |
The CMS Patient Access API: a single authorized patient's own US Core bundle. It fails closed with 501 when no FHIR store is configured, and returns 401 / 404 — never a fabricated bundle — for an unauthorized or absent patient.
Auth Patient / authorized session
| Field | Type | Description |
|---|---|---|
resourceType | string | Always "Bundle". |
type | string | "collection" — the authorized patient's US Core resources. |
entry | array | US Core resources (Patient, Observation, Condition, MedicationRequest, …) scoped to the single authenticated patient only. |
# The session resolves the patient — a patient can only fetch their own data.
curl "https://your-sandbox-origin.example/api/fhir/patient-access" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer $PATIENT_ACCESS_TOKEN"const res = await fetch("/api/fhir/patient-access", {
headers: { Accept: "application/fhir+json" },
credentials: "include",
});
if (res.status === 503) {
// No FHIR store configured — fail-closed, not an empty bundle.
}
const bundle = await res.json();| Status | Meaning |
|---|---|
401 | No authorized patient session. |
404 | No patient record resolves for this session — never a fabricated one. |
403 | The session is not authorized for patient-mediated access. |
501 | Fail-closed: no FHIR store configured — the route refuses rather than return an empty or synthesized bundle. |
Push resources into a configured external FHIR store. This is the one write-shaped route here, and it is the most explicit about failing closed: with no store configured it returns 501 and makes zero network calls.
Auth Staff session
| Name | Type | In | Required | Description |
|---|---|---|---|---|
resources | array | body | Optional | FHIR resources to sync into the store. When omitted the route syncs your organization's pending set. |
curl -X POST "https://your-sandbox-origin.example/api/fhir/gcp-sync" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION" \
-d '{"resources": []}'const res = await fetch("/api/fhir/gcp-sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ resources: [] }),
});
// 501 when no FHIR store is configured — no network call is made.
const result = await res.json();| Status | Meaning |
|---|---|
401 | No valid staff session. |
403 | Session lacks the required scope. |
404 | A referenced resource does not exist in your organization. |
501 | Fail-closed: no external FHIR store configured — the route refuses with zero network calls and no fabricated confirmation. |
Resolve the SMART App Launch context for the current access token: patient, encounter, granted scope, and fhirUser. The access token is the context — there is no separate context store to trust.
Auth SMART access token
| Field | Type | Description |
|---|---|---|
patient | string | null | The launch patient id, when a patient-scope launch granted one. |
encounter | string | null | The launch encounter id, when present. |
scope | string | The space-delimited scopes actually granted to this token. |
fhirUser | string | The FHIR reference of the launching user (e.g. Practitioner/prac_1). |
# The token itself carries the context — pass it as a bearer.
curl "https://your-sandbox-origin.example/api/smart/context" \
-H "Authorization: Bearer $SMART_ACCESS_TOKEN"const res = await fetch("/api/smart/context", {
headers: { Authorization: `Bearer ${accessToken}` },
});
const { patient, encounter, scope, fhirUser } = await res.json();{
"patient": "pat_9f2a41",
"encounter": "enc_7c1e",
"scope": "launch/patient patient/*.read openid fhirUser",
"fhirUser": "Practitioner/prac_1"
}| Status | Meaning |
|---|---|
401 | Missing or invalid SMART access token. |
403 | The token grants no context-resolving scope. |
404 | No context resolves for this token — never a fabricated patient. |
501 | Fail-closed: the SMART launch backing is unconfigured; nothing is synthesized. |
The public CDS Hooks discovery document — the catalogue of clinical decision support services this deployment advertises, across both rule sources. Invocation of an individual service fails closed when its knowledge base is unconfigured.
Auth Public discovery
POST /api/fhir/cds-services/[serviceId] — returns 501 when that service’s knowledge base is unconfigured. A CDS card is a suggestion, never a disposition: the human clinician disposes.| Field | Type | Description |
|---|---|---|
services | array | The advertised CDS services. |
services[].hook | string | The hook that triggers the service (e.g. patient-view, order-select). |
services[].id | string | The service id used in the POST invocation path. |
services[].title | string | Human-readable service title. |
# Public discovery — no auth. Then POST the chosen serviceId to invoke.
curl "https://your-sandbox-origin.example/api/fhir/cds-services"const res = await fetch("/api/fhir/cds-services");
const { services } = await res.json();
// Invoking a service may return 501 if its KB is unconfigured:
// await fetch(`/api/fhir/cds-services/${services[0].id}`, { method: "POST", ... }){
"services": [
{
"hook": "patient-view",
"id": "va-trend-alert",
"title": "Visual-acuity trend advisory",
"description": "Advises on a declining VA trend. Suggestion only."
}
]
}| Status | Meaning |
|---|---|
401 | Invocation (POST) only: no valid session for a scoped service. |
404 | No such serviceId. |
403 | Invocation is not authorized for this service. |
501 | Fail-closed: the service's knowledge base is unconfigured — the POST invocation refuses rather than emit a fabricated card. |
Generate a Consolidated CDA (C-CDA) document for a single encounter — the document-level interop artifact for a referral, a transition of care, or a records request.
Auth Staff session
| Name | Type | In | Required | Description |
|---|---|---|---|---|
encounterId | string | path | Required | The encounter to render as a C-CDA document. Always scoped to your organization. |
# Returns an application/xml C-CDA document for the encounter.
curl "https://your-sandbox-origin.example/api/interop/ccda/enc_7c1e" \
-H "Accept: application/xml" \
-H "Cookie: $SHTEG_SESSION"const res = await fetch(`/api/interop/ccda/${encounterId}`, {
headers: { Accept: "application/xml" },
credentials: "include",
});
const ccdaXml = await res.text(); // C-CDA document| Status | Meaning |
|---|---|
401 | No valid staff session. |
403 | The encounter is outside your organization. |
404 | No such encounter — never a fabricated document. |
501 | Fail-closed: a required document dependency is unconfigured; no document is synthesized. |
Blue Button 2.0 here is an INBOUND, patient-mediated import — a staff-initiated OAuth flow that pulls a patient's CMS claims data in, with the patient's consent. It is not a data-sharing endpoint that exposes shteg.ai data outward. Frame it precisely.
Auth Staff session · scope integrations:read
/authorize; the patient authenticates and consents at CMS; CMS redirects to /callback with a grant that lets shteg.ai import that patient’s CMS claims. Data flows into the system with patient consent — this is not an outbound share of shteg.ai records.| Name | Type | In | Required | Description |
|---|---|---|---|---|
code | string | header | Optional | Callback only: the OAuth authorization code CMS returns after patient consent. |
state | string | header | Optional | Callback only: the CSRF state value minted at /authorize and echoed back. |
# Staff-initiated: returns the CMS authorization URL to hand to the patient.
curl "https://your-sandbox-origin.example/api/interop/bluebutton/authorize" \
-H "Cookie: $SHTEG_SESSION"const res = await fetch("/api/interop/bluebutton/authorize", {
credentials: "include",
});
const { authorizeUrl } = await res.json();
// Hand authorizeUrl to the patient; CMS redirects to /callback on consent.| Status | Meaning |
|---|---|
401 | No valid staff session to initiate the import. |
403 | Session lacks the integrations:read scope. |
404 | Callback: no matching pending authorization for the returned state. |
501 | Fail-closed: Blue Button OAuth credentials are unconfigured — the flow refuses rather than fabricate a grant. |
Here is precisely what this interoperability layer does and does not do in the sandbox tier.
/api/fhir/metadata for the exact interactions; do not overstate write support.$export (Patient + Group kickoff and bulk-status polling) returns 501 with no FHIR store configured.gcp-sync likewise returns 501 and makes zero network calls when the store is unconfigured.501 when its knowledge base is unconfigured.Across the FHIR surface the failure modes are typed and honest — a refusal is never a fabricated success.
| Status | Meaning |
|---|---|
401 | Request is not authenticated — no valid session, or the SMART / job token is missing or invalid. |
403 | Authenticated but the scope is insufficient, or the resource is outside your organization. |
404 | No such resource, encounter, job, or context in your organization — never a fabricated one. |
501 | Fail-closed: no backing FHIR store (or knowledge base) configured — the route refuses with zero network calls and never a simulated bundle, job, or card. |
This surface reads and exports what the EMR sync brings in, and the whole loop is owned by one company that is also a real, registered clearinghouse. Follow either thread.