SMART Backend Services against eClinicalWorks FHIR R4
Your service needs to read patient data from an eClinicalWorks practice at night, with no clinician logged in. The SMART App Launch flow needs a user in a browser, so it does not fit. eClinicalWorks documents a second path for this case, SMART Backend Services. The details below come from the eClinicalWorks developer documentation and the HL7 SMART Backend Services specification.
The shape of the flow
Backend Services is the OAuth 2.0 client credentials grant with one change. The client sends no client secret. It signs a JSON Web Token (JWT) with a private key and sends that JWT as its credential. HL7 calls this asymmetric client authentication. The server holds only your public key, so a copied config file does not contain a reusable secret.
Register the app and a key
Register the app on the eClinicalWorks developer portal. During registration you give a URL that serves your public key as a JWK Set (JWKS). The eClinicalWorks documentation states that the JWKS URL must be reachable by the eClinicalWorks authorization server and must be whitelisted on their servers. The kid in your JWKS must match the kid in every client assertion you send.
The HL7 specification says each JWK carries kty and kid, plus n and e for an RSA key. It also says the JWKS endpoint must be reachable over TLS without client authentication.
Two approvals happen outside your code. The eClinicalWorks documentation states that the app must be authorized for bulk APIs and system scopes on the developer platform. It also states that the EMR organization must authorize access to its patient groups. A practice does this in the EHR under Admin, Product Activation, FHIR APIs, Bulk/Backend Apps. The practice builds a patient group from a saved Registry query. The app can read only patients in enabled groups.
Find the token endpoint
The HL7 specification says to read .well-known/smart-configuration to find the token URL. eClinicalWorks documents a metadata call instead: GET {issuer}/metadata?_format=json. The issuer URL comes from the launch request or from the Add Customer window in the developer portal. Each practice has its own issuer, so store one token URL per practice.
Build the client assertion
The assertion is a JWT. The HL7 specification sets these claims.
| Claim | Value |
|---|---|
iss | Your client_id |
sub | Your client_id, same as iss |
aud | The token URL you will post to |
exp | Expiry, no more than five minutes in the future |
jti | A unique value, used once |
The header carries alg, kid and typ with the value JWT. HL7 lets a client use RS384 or ES384. eClinicalWorks is narrower. Its documentation states: "We only support RS384 alg for asymmetric authentication." Sign with RS384 and an RSA key.
{ "alg": "RS384", "kid": "key-2026-11", "typ": "JWT" }
{
"iss": "your-client-id",
"sub": "your-client-id",
"aud": "https://{ehr_token_url}",
"exp": 1794140000,
"jti": "a-random-value-used-once"
}
Generate a new jti for each request. The HL7 specification says the server must reject a repeated jti from the same issuer within the assertion lifetime.
Request the token
POST https://{ehr_token_url}
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=system/Patient.read system/Group.read
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={signed JWT}
The scope list must be a subset of the scopes registered for the app. eClinicalWorks states that system/Group.read must be included in every token request for bulk Group requests. It also states that this scope must not be included for the Backend Single Patient API.
The response carries access_token, token_type, expires_in and scope. The eClinicalWorks example shows expires_in of 300 seconds and no refresh token. That matches the HL7 specification, which says expires_in should not exceed 300 and refresh tokens should not be issued. Request a fresh token at the start of each job instead of caching one.
eClinicalWorks documents these token errors. invalid_client (401) covers an unknown client, a kid mismatch, or a JWKS URL that is unreachable or not whitelisted. unsupported_grant_type (400) covers any grant other than client_credentials. invalid_grant (400) covers a scope that is unknown or not approved. invalid_request (400) covers a missing or repeated parameter.
What the token lets you read
Send the token as Authorization: Bearer {access_token}. The documented use is bulk export of a patient group.
GET {fhir_base_url}/{org_id}/Group/{group_id}/$export
Prefer: respond-async
Accept: application/fhir+json
Authorization: Bearer {access_token}
The documentation states that respond-async and application/fhir+json are the only accepted values for those two headers. Optional parameters are _outputFormat (default application/fhir+ndjson), _type and _since. The kick-off returns 202 with a content-location polling URL and retry-after: 120. Poll that URL until you get 200 with an output array of NDJSON file URLs and an error array of OperationOutcome files. An identical export request is rejected while one is in progress. A DELETE on the polling URL cancels the job.
The documentation names a Backend Single Patient API but gives it no separate resource list. The Read API section lists 61 resources for provider apps. The public documentation does not state which of those a backend app can read. Two things bound what comes back: the scopes approved for your app, and the patient groups each practice enabled.
Two limits matter for a nightly job. eClinicalWorks states a limit of 500 calls per minute per base URL across the FHIR resource API, /authorize and /token, measured per practice code. Past that limit, every request from the app is blocked for the rest of the minute with HTTP 429. The documentation also states that CORS headers are not supported, so the flow must run server-side.
Sandbox and contract
The portal describes a sandbox for testing before an app is published. For sandbox credentials the documentation says to contact the FHIR support team. The portal also states that apps with contract-required scopes go through a scope review and a pricing agreement with eClinicalWorks. The public pages do not state which scopes require a contract or what the fees are.
For working code that builds the assertion and runs the export loop, the TheFhirStorm connectors in Python and TypeScript are open source.