Skip to content

Understanding Endpoint Capabilities

The Endpoint Capabilities endpoint returns everything a FHIR server publishes about itself: which resource types it supports, which interactions (read/write) are available per resource, and the full SMART on FHIR authorization configuration.

This guide explains how to interpret that payload so you can answer questions like:

  • Can I read Observations from this endpoint?
  • Can I write Appointments to this endpoint?
  • What audience does this endpoint serve — patients, providers, or both?
  • Which OAuth flow should my app use?

The response shape

GET /api/v1/endpoints/:id/capabilities

Returns three things stacked together:

  1. summary — pre-extracted scalar fields for fast consumption
  2. capabilityStatement — the raw FHIR CapabilityStatement from /metadata
  3. smartConfiguration — the raw /.well-known/smart-configuration document

The summary is enough for most filtering and display. Reach into the raw payloads when you need fine-grained detail the summary doesn't surface (per-resource read/write, specific SMART extensions, etc).


Per-resource read/write support

The summary.supportedResources array tells you which resource types the server exposes, but not what you can do with each one. To determine read vs write capability per resource, walk the raw capabilityStatement:

capabilityStatement.rest[0].resource[]
  ├─ type         → "Observation"
  └─ interaction[]
      ├─ code: "read"
      ├─ code: "search-type"
      └─ code: "create"

Group the interaction codes into read-capable and write-capable buckets:

BucketFHIR interaction codes
Readread, vread, search-type, history-instance, history-type
Writecreate, update, patch, delete

A resource with at least one read code is readable; one with at least one write code is writable. Many production endpoints are read-only — always check before assuming you can POST.

Example: parse read/write per resource

typescript
interface ResourceAccess {
  type: string;
  readable: boolean;
  writable: boolean;
}

const READ = new Set(['read', 'vread', 'search-type', 'history-instance', 'history-type']);
const WRITE = new Set(['create', 'update', 'patch', 'delete']);

async function getResourceAccess(endpointId: string): Promise<ResourceAccess[]> {
  const res = await fetch(`https://fhir-api.luxera.io/api/v1/endpoints/${endpointId}/capabilities`);
  const { data } = await res.json();
  const resources = data.capabilityStatement?.rest?.[0]?.resource ?? [];

  return resources.map((r: any) => {
    const codes = new Set((r.interaction ?? []).map((i: any) => i.code));
    return {
      type: r.type,
      readable: [...READ].some(c => codes.has(c)),
      writable: [...WRITE].some(c => codes.has(c)),
    };
  });
}
python
import requests

READ = {"read", "vread", "search-type", "history-instance", "history-type"}
WRITE = {"create", "update", "patch", "delete"}

def get_resource_access(endpoint_id: str):
    r = requests.get(f"https://fhir-api.luxera.io/api/v1/endpoints/{endpoint_id}/capabilities")
    data = r.json()["data"]
    resources = (data.get("capabilityStatement") or {}).get("rest", [{}])[0].get("resource", [])

    out = []
    for res in resources:
        codes = {i["code"] for i in res.get("interaction", [])}
        out.append({
            "type": res["type"],
            "readable": bool(codes & READ),
            "writable": bool(codes & WRITE),
        })
    return out

Understanding the access field

On every endpoint, the top-level access field tells you the audience — who the endpoint is intended for:

ValueMeaning
["patient"]Patient-facing (standalone launch, patient-level scopes)
["provider"]Clinician-facing (EHR launch, user-level scopes)
["patient", "provider"]Both audiences supported
[]Unknown — endpoint has not been probed or does not publish SMART config

When the directory probes an endpoint's SMART configuration, it infers the audience from launch mode and scope patterns:

  • Patient indicators: launch-standalone capability, context-standalone-patient, or any patient/* scope
  • Provider indicators: launch-ehr capability, context-ehr-patient, or any user/* scope

Both lists can be true — many Epic and Cerner endpoints support both patient and provider audiences.


SMART capability codes

The summary.smartCapabilities array is a flat list of SMART conformance codes. They fall into a few natural groups:

Launch Modes

How your app is launched:

CodeMeaning
launch-ehrEHR-initiated launch — your app is opened from within the EHR
launch-standaloneStandalone launch — your app opens the EHR for auth

Client Types

Which OAuth client authentication schemes the server accepts:

CodeUse when
client-publicMobile/SPA apps that can't keep a secret
client-confidential-symmetricServer apps using a shared client secret
client-confidential-asymmetricServer apps using JWT client assertion (backend services)

Single Sign-On

CodeMeaning
sso-openid-connectServer returns an OpenID Connect id_token — use it to identify the end user

Launch Context

What context the server will pass to your app after launch:

CodeMeans the server will pass…
context-ehr-patientThe current patient when launched from the EHR
context-ehr-encounterThe current encounter when launched from the EHR
context-standalone-patientA patient picker for standalone launches
context-standalone-encounterAn encounter picker for standalone launches
context-bannerPatient banner styling hints
context-styleUI style hints (fonts, colors)

Permissions / Scopes

Which scope patterns the server accepts:

CodeMeans the server supports…
permission-offlineoffline_access scope (refresh tokens)
permission-patientpatient/* scopes (patient-level data access)
permission-useruser/* scopes (user-level data access)
permission-v1SMART v1 scope syntax (patient/Observation.read)
permission-v2SMART v2 scope syntax (patient/Observation.rs)

Most production endpoints accept v1 syntax. v2 is newer and adds finer-grained controls.

Authorization endpoint protocol

CodeMeaning
authorize-postThe authorization endpoint accepts POST as well as GET — useful for long request URIs

Choosing your OAuth flow

Based on the capability flags, here's how to pick a flow:

Your app is…UseRequired capabilities
A mobile app or SPAStandalone launch, PKCE, public clientlaunch-standalone, client-public, permission-patient
A web app launched from the EHREHR launch, confidential symmetriclaunch-ehr, client-confidential-symmetric, permission-user
A backend service (no user)Client credentials, asymmetric JWTclient-confidential-asymmetric, SMART Backend Services grant type

The grant_types_supported field in smartConfiguration tells you which OAuth 2.0 grant types the authorization server accepts (authorization_code, client_credentials, refresh_token, etc.).


What isn't in summary

The summary fields are optimized for browsing and filtering. If you need any of the following, read from the raw capabilityStatement or smartConfiguration:

  • Per-resource interactions (see above)
  • searchParam lists (which query parameters each resource supports)
  • operation definitions (custom $ operations like $everything)
  • Server-level extensions
  • The full scopes_supported list
  • jwks_uri, introspection_endpoint, and other OAuth metadata

The raw documents are complete passthroughs of what the server published — the directory does not modify or filter them.


Probe status

The probeStatus field indicates how recently the directory contacted the endpoint:

StatusMeaning
successBoth /metadata and /.well-known/smart-configuration responded
partialOnly one of the two responded
failedNeither responded. Check probeFailureReason for the cause.

Common failure reasons:

  • auth_required — server is alive but requires credentials (endpoint stays as listed, not marked unresponsive)
  • dns_failure, timeout, network_error — endpoint is likely down (marked as unresponsive)

Data from a failed probe may be stale — treat it as a hint, not ground truth.

Built by Luxera Software