These endpoints connect a clinic’s external EMR to ShtegMed over sanctioned APIs only: an OAuth 2.0 authorization-code flow for browser-consent EMRs, SMART Backend Services for non-browser SMART sync, and a partner-API path for non-browser vendor sync. A connection is a durable, tenant-scoped record; a pull upserts FHIR R4 resources into a FHIR R4 store. Secrets are never returned on a read, and every pull fails closed when its credential is unconfigured.
This is the sandbox surface — real logic, zero live data. Production access is a separate approval. A browser-OAuth EMR is not reachable until its client is registered (canAuthorize: false until a client_id exists), and the two non-browser pulls refuse with a typed 501, zero network calls, and never a synthetic FHIR resource until their credentials are configured. There is deliberately no session- or portal-scrape path — only sanctioned API and SMART flows exist.
Every request is JSON over HTTPS against your sandbox origin. Every API request is authenticated before it reaches a handler; connection reads and pulls are additionally scope-checked.
Connections are bound to your clinic; one clinic can never read, authorize, or pull against another’s connection. Requests are automatically scoped to your organization from your credentials — you never pass an organization or clinic identifier.
Connection and callback routes require a Staff session plus integrations:read . The browser authorize handoff needs only a valid Staff session. The partner-API pull is additionally scoped to your organization from your credentials.
A connection read returns vendor, status, and a canAuthorize flag — never a client secret, access token, or refresh token. Tokens live server-side only.
A pull whose credential (SMART assertion or partner creds) is unconfigured returns a typed 501 with zero network calls. The service never simulates a successful sync or writes a fabricated FHIR resource.
A connection carries a vendor identifier and, for browser-OAuth EMRs, an OAuth client_id. It progresses pending → connected, or sits at needs-registration until a client is registered.
| Name | Type | In | Required | Description |
|---|---|---|---|---|
generic-fhir | enum | body | Optional | A generic SMART-on-FHIR R4 server reached over the browser OAuth 2.0 auth-code flow. |
browser-oauth | enum | body | Optional | A supported EMR reached over the browser OAuth 2.0 auth-code flow; requires a registered per-clinic client_id. |
smart-backend | enum | body | Optional | A supported EMR reached over SMART Backend Services (non-browser) with a signed client assertion — see the SMART Backend Services pull below. |
partner-api | enum | body | Optional | A supported EMR reached over a partner API (non-browser) with server-side partner credentials — see the Partner API pull below. |
pendingCreated with a vendor and (for browser EMRs) a client_id. Awaiting the OAuth consent round-trip.
connectedThe callback exchanged the code for tokens. Tokens live server-side; the read only reports the status.
needs-registrationNo client_id yet, so canAuthorize is false. This is an expected state, not a failure — authorize is simply not reachable until a client is registered.
Return the clinic's connection records — vendor, status, and a canAuthorize flag. Secrets are never returned.
Staff session + integrations:read
| Field | Type | Description |
|---|---|---|
connections | array | The clinic-scoped connection records. |
connections[].id | string | The connection identifier. |
connections[].vendor | string | One of the connection-kind enum values. |
connections[].status | string | pending | connected | needs-registration. |
connections[].canAuthorize | boolean | True only when a client_id exists and the authorize flow is reachable. |
curl "https://your-sandbox-origin.example/api/emr-sync/connections" \
-H "Cookie: $SHTEG_SESSION"const res = await fetch("/api/emr-sync/connections", {
credentials: "include",
});
const { connections } = await res.json();{
"connections": [
{
"id": "conn_3a91",
"vendor": "smart-backend",
"status": "needs-registration",
"canAuthorize": false
},
{
"id": "conn_7f20",
"vendor": "generic-fhir",
"status": "connected",
"canAuthorize": true
}
]
}| Status | Meaning |
|---|---|
401 | No valid Staff session — the request was not authenticated. |
403 | Session lacks the integrations:read scope. |
503fail-closed | Fail-closed: a required dependency is unconfigured; nothing is synthesized. |
Example only — sandbox-shaped. Secrets and tokens are never returned; needs-registration is an honest state, not a failure.
curl -X GET https://api.shteg.ai/api/emr-sync/connections \
-H "Authorization: Bearer $SHTEG_TOKEN" \
-H "Cookie: $SHTEG_SESSION (scope integrations:read)"Create a pending connection record carrying a vendor and, for browser-OAuth EMRs, an OAuth client_id. The connection stays at needs-registration until a client_id is present.
Staff session + integrations:read
| Name | Type | In | Required | Description |
|---|---|---|---|---|
vendor | enum | body | Required | One of: generic-fhir, browser-oauth, smart-backend, partner-api. |
clientId | string | body | Optional | The vendor OAuth client_id. Present → status pending; absent → status needs-registration (canAuthorize false). |
curl -X POST "https://your-sandbox-origin.example/api/emr-sync/connections" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION" \
-d '{"vendor": "generic-fhir", "clientId": "oauth_client_9f2a"}'const res = await fetch("/api/emr-sync/connections", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ vendor: "generic-fhir", clientId: "oauth_client_9f2a" }),
});
const { connection } = await res.json();{
"connection": {
"id": "conn_7f20",
"vendor": "generic-fhir",
"status": "pending",
"canAuthorize": true
}
}| Status | Meaning |
|---|---|
400 | Missing or invalid vendor (not in the enum). |
401 | No valid Staff session. |
403 | Session lacks the integrations:read scope. |
503fail-closed | Fail-closed: a required dependency is unconfigured; nothing is created. |
Example only — sandbox-shaped. A client_id present → status pending (canAuthorize true); absent → needs-registration.
curl -X POST https://api.shteg.ai/api/emr-sync/connections \
-H "Authorization: Bearer $SHTEG_TOKEN" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION (scope integrations:read)" \
-d '{
"vendor": "generic-fhir",
"clientId": "oauth_client_9f2a"
}'Mint a single-use state (15-minute TTL) and 302-redirect the browser to the vendor's consent screen. This is the browser handoff for browser-OAuth EMRs and generic-FHIR servers.
Staff session
client_id (canAuthorize: true). A connection at needs-registration is not a failure — the browser handoff is simply not offered until the per-EMR client is registered.| Name | Type | In | Required | Description |
|---|---|---|---|---|
connectionId | string | path | Required | The connection to authorize (query parameter). Must belong to your clinic. |
# The browser opens this; -i shows the 302 to vendor consent
curl -i "https://your-sandbox-origin.example/api/emr-sync/authorize?connectionId=conn_7f20" \
-H "Cookie: $SHTEG_SESSION"// A staff member is navigated to the authorize route;
// the server mints a single-use state and 302-redirects to the vendor.
window.location.href =
`/api/emr-sync/authorize?connectionId=${connectionId}`;HTTP/1.1 302 Found
Location: https://vendor.example.com/oauth2/authorize?response_type=code
&client_id=oauth_client_9f2a
&redirect_uri=https%3A%2F%2Fyour-sandbox-origin.example%2Fapi%2Femr-sync%2Fcallback
&scope=launch%2Fpatient+patient%2F*.read+offline_access
&state=st_5d8a41_singleuse| Status | Meaning |
|---|---|
401 | No valid Staff session. |
403 | Connection belongs to another clinic, or has no client_id (canAuthorize false). |
404 | No such connection in your tenant. |
503fail-closed | Fail-closed: the vendor registry entry is unconfigured; the redirect is refused. |
Consume the single-use state and exchange the authorization code for tokens via the vendor registry's token endpoint. State is single-use; an invalid or expired state returns 400.
Staff session + integrations:read
| Name | Type | In | Required | Description |
|---|---|---|---|---|
code | string | path | Required | The vendor authorization code (query parameter), exchanged for tokens server-side. |
state | string | path | Required | The single-use state minted at authorize (query parameter). Consumed once; expired or reused → 400. |
| Field | Type | Description |
|---|---|---|
connectionId | string | The connection that was just connected. |
vendor | string | The connection-kind enum value for this connection. |
status | string | connected after a successful token exchange. |
# The vendor redirects the browser back here with code + state
curl "https://your-sandbox-origin.example/api/emr-sync/callback?code=AUTH_CODE&state=st_5d8a41_singleuse" \
-H "Cookie: $SHTEG_SESSION"{
"connectionId": "conn_7f20",
"vendor": "generic-fhir",
"status": "connected"
}| Status | Meaning |
|---|---|
400 | Invalid or expired state — single-use and past its 15-minute TTL, or reused. No tokens are exchanged. |
401 | No valid Staff session. |
403 | Session lacks the integrations:read scope, or the state belongs to another clinic. |
503fail-closed | Fail-closed: the vendor token endpoint is unconfigured; the exchange is refused with zero network calls. |
The sanctioned non-browser SMART sync: obtain a SMART Backend Services token, pull FHIR R4 resources, and upsert them into the FHIR R4 store. No consent screen — it authenticates with a signed client assertion.
Staff session
501 with zero network calls. It never writes a synthetic FHIR resource. The pull covers Patient, Encounter, Observation, Condition, MedicationRequest, and AllergyIntolerance.| Name | Type | In | Required | Description |
|---|---|---|---|---|
connectionId | string | body | Required | The smart-backend connection to pull against. Must belong to your clinic. |
since | string | body | Optional | Optional ISO-8601 lower bound; only resources updated on or after it are pulled. |
| Field | Type | Description |
|---|---|---|
ok | boolean | True when the pull completed. |
upserted | object | Per-resource-type counts written to the FHIR store. |
fhirStore | string | The FHIR R4 store the resources were upserted into. |
curl -X POST "https://your-sandbox-origin.example/api/emr-sync/smart/pull" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION" \
-d '{"connectionId": "conn_3a91", "since": "2026-07-01T00:00:00Z"}'const res = await fetch("/api/emr-sync/smart/pull", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ connectionId: "conn_3a91" }),
});
// 501 with a typed refusal if the SMART assertion is unconfigured
const result = await res.json();{
"ok": true,
"upserted": {
"Patient": 12,
"Encounter": 34,
"Observation": 210,
"Condition": 18,
"MedicationRequest": 27,
"AllergyIntolerance": 9
},
"fhirStore": "shteg-fhir-store"
}| Status | Meaning |
|---|---|
400 | Missing or invalid connectionId. |
401 | No valid Staff session. |
403 | The connection belongs to another clinic. |
501 | Fail-closed: the SMART Backend Services assertion is unconfigured; the pull refuses with zero network calls and writes no FHIR resource. |
503fail-closed | Fail-closed: the FHIR R4 store is unreachable; nothing is upserted. |
Example only — sandbox-shaped. An unconfigured SMART assertion fails closed with a typed 501.
curl -X POST https://api.shteg.ai/api/emr-sync/smart/pull \
-H "Authorization: Bearer $SHTEG_TOKEN" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION (staff session)" \
-d '{
"connectionId": "conn_3a91",
"since": "2026-07-01T00:00:00Z"
}'Sync from a supported EMR's partner API into the FHIR R4 store for a date or date-range. Non-browser — it authenticates with server-side partner credentials, not a consent screen.
Staff session + integrations:read
501 — zero network calls, never a synthetic FHIR resource.| Name | Type | In | Required | Description |
|---|---|---|---|---|
date | string | body | Optional | A single ISO-8601 date to sync. Provide either date or a from/to range. |
from | string | body | Optional | ISO-8601 lower bound of a date-range sync. |
to | string | body | Optional | ISO-8601 upper bound of a date-range sync. |
curl -X POST "https://your-sandbox-origin.example/api/emr-sync/partner/pull" \
-H "Content-Type: application/json" \
-H "Cookie: $SHTEG_SESSION" \
-d '{"from": "2026-07-01", "to": "2026-07-14"}'const res = await fetch("/api/emr-sync/partner/pull", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ date: "2026-07-14" }),
});
// 501 with a typed refusal if partner creds are unconfigured
const result = await res.json();{
"ok": true,
"range": { "from": "2026-07-01", "to": "2026-07-14" },
"upserted": {
"Patient": 8,
"Encounter": 22,
"Observation": 96
},
"fhirStore": "shteg-fhir-store"
}| Status | Meaning |
|---|---|
400 | No usable date or from/to range supplied. |
401 | No valid session — the request was not authenticated. |
403 | Session lacks the integrations:read scope. |
501 | Fail-closed: the partner-API credentials are unconfigured; the pull refuses with zero network calls and writes no FHIR resource. |
503fail-closed | Fail-closed: the FHIR R4 store is unreachable; nothing is upserted. |
Two shapes of integration exist here, and both are honest about what they require. Neither ever scrapes a session or a portal.
Browser-OAuth EMRs and generic-FHIR servers use the browser auth-code flow. Each requires a per-EMR client registration before authorize is reachable — canAuthorize is false and the connection reads needs-registration until a client_id exists. That is an honest gate, not a failure.
The SMART Backend Services pull (signed client assertion) and the partner-API pull (server-side partner credentials) authenticate without a consent screen. Both pull routes fail closed with a typed 501 when their credential is unconfigured — zero network calls, and never a synthetic FHIR resource.
The connection lifecycle, OAuth handoff, and both sanctioned pulls are wired end to end. Browser-OAuth EMRs wait on a per-EMR client registration, and the two non-browser pulls refuse with a typed 501 until the SMART assertion or partner credentials are configured — never a fabricated FHIR resource.