FHIR API Integration: How It Works End to End
A practical walk-through of integrating with a FHIR REST API, from reading the CapabilityStatement to authentication, searching, paging, error handling, rate limits and testing.
Key takeaways
- Start every integration by reading the server's CapabilityStatement and SMART configuration; they tell you what is actually supported.
- Server-to-server integrations typically authenticate with SMART Backend Services: a signed JWT exchanged for a short-lived access token.
- Search results arrive as Bundles that may be paged; always follow the next link rather than constructing page URLs yourself.
- Treat OperationOutcome as the error contract, design for 429 and 5xx responses, and test against both sandboxes and validators before production.
The integration at a glance
Integrating with a FHIR API follows a predictable sequence regardless of whether the server is an EHR, a payer platform or your own FHIR server:
- Discover the server's capabilities and security endpoints.
- Register your client and obtain credentials.
- Request an access token.
- Read and search resources, following references and pages.
- Write data back, if the use case and server allow it.
- Handle errors, throttling and retries.
- Test against sandboxes, validators and, finally, production.
The examples below use FHIR R4 and a placeholder base URL of https://fhir.example.org/r4. If FHIR concepts such as resources and Bundles are new to you, read What is FHIR? first.
Step 1: Discover capabilities
Every conformant FHIR server publishes a CapabilityStatement at [base]/metadata. It lists the FHIR version, supported resource types, interactions (read, search-type, create, update), search parameters, operations and the profiles the server claims to support. Read it before you write code; it is the closest thing to an authoritative contract.
GET /r4/metadata HTTP/1.1
Host: fhir.example.org
Accept: application/fhir+jsonFor servers that implement SMART, also fetch [base]/.well-known/smart-configuration. It returns the authorization and token endpoints, supported scopes and the client authentication methods the server accepts. Note that a CapabilityStatement says what the software can do; the scopes granted to your client and the server's data may still limit what you actually receive.
Step 2: Authenticate
FHIR itself does not define security, but the SMART App Launch implementation guide is the standard approach. Choose the flow by who is using the integration:
- SMART App Launch (authorization code) for apps used by a patient or clinician, launched standalone or from inside the EHR. See the SMART on FHIR guide.
- SMART Backend Services for system-to-system integrations with no user present, such as nightly syncs, analytics pipelines or Bulk Data export.
With Backend Services, you register a public key (usually as a JWK Set URL) with the server operator. To get a token, your service signs a short-lived JWT with its private key (RS384 or ES384) and posts it to the token endpoint using the OAuth 2.0 client credentials grant:
POST /oauth2/token HTTP/1.1
Host: auth.example.org
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=system/Patient.rs system/Observation.rs
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJSUzM4NCIsImtpZCI6IjEifQ...The response contains an access_token and expires_in. Cache the token, reuse it until shortly before expiry, then request a new one. SMART v2 scopes use the .rs (read and search) style shown above; older servers may only accept v1 scopes such as system/Patient.read. Our FHIR security guide covers key management and scope design.
Step 3: Search, include and page
Most integration traffic is search. Search parameters are defined per resource type, and servers support different subsets, so check the CapabilityStatement. Useful patterns include token searches (code=http://loinc.org|4548-4), date prefixes (date=ge2026-01-01), reference chaining (subject:Patient.identifier=...) and result-control parameters such as _count, _sort, _lastUpdated, _elements and _summary.
Use _include and _revinclude to pull related resources in a single round trip. For example, fetching a patient's active medication requests together with the referenced Medication resources:
GET /r4/MedicationRequest?patient=004512&status=active&_include=MedicationRequest:medication&_count=50 HTTP/1.1
Host: fhir.example.org
Accept: application/fhir+json
Authorization: Bearer eyJhbGciOi...{
"resourceType": "Bundle",
"type": "searchset",
"link": [
{ "relation": "self", "url": "https://fhir.example.org/r4/MedicationRequest?patient=004512&status=active&_include=MedicationRequest:medication&_count=50" },
{ "relation": "next", "url": "https://fhir.example.org/r4?_getpages=8c1f2e&_getpagesoffset=50&_count=50" }
],
"entry": [
{
"fullUrl": "https://fhir.example.org/r4/MedicationRequest/mr-1",
"resource": { "resourceType": "MedicationRequest", "id": "mr-1", "status": "active", "intent": "order", "medicationReference": { "reference": "Medication/med-9" }, "subject": { "reference": "Patient/004512" } },
"search": { "mode": "match" }
},
{
"fullUrl": "https://fhir.example.org/r4/Medication/med-9",
"resource": { "resourceType": "Medication", "id": "med-9" },
"search": { "mode": "include" }
}
]
}Note search.mode: "match" entries satisfy your query, "include" entries came along because of _include. When a next link is present, follow it exactly as given; page URLs are opaque and server-specific. Bundle.total is optional and often omitted for performance, so do not rely on it to know when you are done. For very large datasets, switch to the Bulk Data $export operation instead of paging through millions of results.
Step 4: Write data safely
Write access is where EHR integrations differ most. Many EHR FHIR APIs are read-heavy, with write support limited to specific resources and workflows, so confirm write capabilities early. Good practice when writing:
- Validate resources against the target profiles before sending.
- Use conditional create (If-None-Exist header) to avoid duplicates when retrying.
- Use ETags with If-Match on updates to prevent overwriting someone else's change; the server returns 412 Precondition Failed on a version conflict.
- Use a transaction Bundle when several resources must be created together or not at all.
Step 5: Handle errors and rate limits
FHIR servers report problems with an HTTP status code and, usually, an OperationOutcome resource describing each issue with a severity, a code and human-readable diagnostics:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/fhir+json
{
"resourceType": "OperationOutcome",
"issue": [
{
"severity": "error",
"code": "required",
"diagnostics": "Observation.status: minimum required = 1, but only found 0",
"expression": ["Observation.status"]
}
]
}| Status | Typical meaning | Client response |
|---|---|---|
| 400 | Malformed request or unsupported search parameter | Fix the request; do not retry unchanged |
| 401 / 403 | Token missing, expired, or lacking scope | Refresh the token once; if still denied, check scopes and registration |
| 404 / 410 | Resource not found or deleted | Treat as data state, not a transient failure |
| 409 / 412 | Version conflict | Re-read the resource, merge, and retry with the new ETag |
| 422 | Resource failed validation or business rules | Log the OperationOutcome and fix the mapping |
| 429 | Too many requests | Back off, honoring Retry-After if present |
| 500 / 503 | Server error or unavailability | Retry with exponential backoff and jitter; alert if persistent |
Rate limits are set by each server operator, not by the FHIR specification, and EHR vendors document their own policies. Build a client-side throttle, prefer fewer and richer queries (with _include and _elements) over many small ones, and schedule heavy jobs off-peak.
Step 6: Test before production
A sound test plan uses several layers:
- Public sandboxes and reference servers, such as a local HAPI FHIR instance, for early development.
- Vendor sandboxes for the specific EHRs you target, since behavior differs; see the Epic and Oracle Health integration pages.
- The HL7 FHIR Validator to check resources against profiles, and Inferno to exercise US Core and SMART behavior.
- Production-like data volumes to surface paging, timeout and throttling issues before go-live.
If you would rather hand this work to specialists, the FHIR API development service page explains what to expect, and the FHIR API development companies directory lists firms that offer it.
Frequently asked questions
What is the CapabilityStatement in FHIR?
It is a FHIR resource, returned from [base]/metadata, that describes what a server supports: FHIR version, resource types, interactions, search parameters, operations and profiles. Clients use it to discover capabilities before integrating.
Which authentication method should a server-to-server FHIR integration use?
SMART Backend Services is the standard approach: the client signs a JWT with its private key and exchanges it for a short-lived access token using the OAuth 2.0 client credentials grant. Some servers also offer proprietary methods.
How does paging work in FHIR search?
The server returns a searchset Bundle with a link whose relation is "next" when more results exist. The client follows that URL until no next link is returned. Page size can be suggested with _count, but servers may cap it.
What is the difference between _include and _revinclude?
_include adds resources that the matched results reference (for example, the Medication referenced by a MedicationRequest). _revinclude adds resources that reference the matched results (for example, Provenance resources pointing at them).
Do all FHIR servers support the same search parameters?
No. Servers support different subsets, and implementation guides such as US Core specify which ones are mandatory for conformance. Always check the CapabilityStatement.
References
- HL7 FHIR R4: RESTful API
- HL7 FHIR R4: Search
- HL7 FHIR R4: OperationOutcome
- SMART App Launch Implementation Guide
- FHIR Bulk Data Access IG
- Inferno (ONC test tools)
FHIR® is a registered trademark of HL7 International. This guide is independent educational content and is not legal or compliance advice.
Related guides
- SMART on FHIR Explained: App Launch, Scopes and Backend ServicesSMART App Launch is how third-party apps plug into EHRs securely. This guide walks through both launch flows, discovery, scopes, PKCE, backend services and how CDS Hooks fits in.
- FHIR Security Best Practices: Authorization, Audit and API HardeningHow to secure a FHIR API end to end: SMART on FHIR authorization, least-privilege scopes, signed JWT backend authentication, audit logging and the API flaws attackers actually exploit.
- FHIR Implementation: A Phased PlaybookA step-by-step plan for implementing FHIR, from discovery and implementation guide selection through mapping, terminology, testing, go-live and ongoing operations, plus the challenges that derail projects.
- FHIR Resources Explained: Structure, Categories and Key ResourcesA practical tour of FHIR resources: what every resource contains, how they are grouped, the ones you will meet in almost every integration, and how references, maturity levels and profiles fit together.
Related FHIR services
- FHIR API DevelopmentHow FHIR API development services design and build compliant FHIR REST APIs: profiles, CapabilityStatements, SMART auth, testing and choosing a capable team.
- FHIR IntegrationWhat FHIR integration services include, how projects are delivered, technical risks to plan for, and the questions to ask before hiring an integration partner.
- EHR/EMR IntegrationEHR/EMR integration services explained: connecting to Epic, Oracle Health, athenahealth and others via FHIR, HL7 v2 and vendor APIs, and choosing a partner.
Find the Right FHIR Integration Partner for Your Healthcare Project
Tell us what you need to build or connect. We review every request and share a short list of relevant vendors from our directory — no obligation.