App launch & PKCE
Ava apps are public SMART App Launch 2.2 clients using the EHR launch: the flow always starts inside Ava EMR, with a clinician (and patient chart) already in context. Your app never holds a client secret; authorization-code interception is mitigated by PKCE S256 instead.
Before hand-rolling this flow, consider adapting the runnable single-file
client in examples/smart-test-app/ — it performs every step below and
prints each intermediate result. This guide explains what it is doing.
The flow at a glance
- Ava EMR opens your launch URI with
issandlaunchparameters. - You fetch the SMART discovery document from the issuer.
- You redirect the browser to the authorization endpoint with a PKCE challenge.
- The user approves your requested scopes.
- The EMR redirects back to your redirect URI with an authorization code.
- You exchange the code (plus your PKCE verifier) at the token endpoint.
- You call the FHIR API with the access token, scoped to the launched patient context.
1. The EMR opens your launch URI
GET {your_launch_uri}?iss=https://sandbox.avaemr.ca&launch=<opaque launch token>
iss— the FHIR authorization issuer. Verify it is an issuer you expect before proceeding; do not blindly followissvalues.launch— an opaque, single-use token binding this authorization to the clinician's current EMR session and open patient chart. Pass it through unchanged in step 3.
Browsing to your app directly (without a launch) is not supported — the Ava sandbox implements the EHR launch only, so show a friendly waiting page.
2. Discover the authorization endpoints
curl "{iss}/.well-known/smart-configuration" -H "Accept: application/json"
The response includes authorization_endpoint, token_endpoint,
jwks_uri, and the supported capabilities and PKCE methods. Always resolve
endpoints from discovery rather than hardcoding them.
3. Redirect to the authorization endpoint
Generate a PKCE pair first:
code_verifier— a high-entropy random string (43–128 characters), stored in the user's session.code_challenge—BASE64URL(SHA256(code_verifier)), sent now.
Then redirect the browser:
{authorization_endpoint}?
response_type=code
&client_id={client_id}
&redirect_uri={your_redirect_uri}
&scope=launch openid fhirUser patient/Patient.r
&state={random anti-CSRF value}
&aud={iss}
&launch={launch token from step 1}
&code_challenge={code_challenge}
&code_challenge_method=S256
scopemust be a subset of the scopes in your app's deployed permission schema;launchis always required.audmust be the FHIR issuer from step 1 — the EMR rejects mismatches.redirect_urimust exactly match one of the URIs registered for your app. In the sandbox, HTTP redirect URIs must use a loopback IP literal (127.0.0.1, notlocalhost).
The user sees Ava's authorization screen listing each requested scope with the rationale you provided in your permission schema, and approves or declines.
4–5. Receive the authorization code
GET {your_redirect_uri}?code=<authorization code>&state=<your state value>
Reject the response unless state matches the value you sent. The code is
short-lived and single-use — exchange it immediately.
6. Exchange the code for tokens
curl -X POST "{token_endpoint}" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d "code=<authorization code>" \
-d "redirect_uri={your_redirect_uri}" \
-d "client_id={client_id}" \
-d "code_verifier=<your stored code_verifier>"
No Authorization header and no client secret — the code_verifier proves
you are the client that started the flow. A successful response looks like:
{
"access_token": "<bearer token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch openid fhirUser patient/Patient.r",
"id_token": "<OpenID Connect JWT>",
"patient": "<FHIR Patient id for the launched chart>"
}
scopeis the set actually granted — it may be narrower than requested. Respect it.patientcarries the launch context: the id of the patient chart open in the EMR. Yourpatient/scopes are confined to this patient.id_token(present withopenid) identifies the signed-in clinician; withfhirUserit includes afhirUserclaim pointing at their FHIR resource. Validate it against the issuer'sjwks_uri.
7. Call the FHIR API
curl "{fhir_base_url}/Patient/{patient id from the token response}" \
-H "Authorization: Bearer $SMART_ACCESS_TOKEN" \
-H "Accept: application/fhir+json"
Access tokens are short-lived. When one expires, complete a new EHR launch — public Ava clients are not issued refresh tokens.
Common failures
| Symptom | Likely cause |
|---|---|
invalid_scope at authorization |
A requested scope is not in your deployed permission schema, or is not implemented (for example, patient/Observation.* does not exist). |
| Redirect URI error at authorization | The redirect_uri does not exactly match a registered URI — check scheme, host, port, path, and the loopback-IP rule. |
invalid_grant at token exchange |
The code expired, was already used, or the code_verifier does not match the challenge. Restart the launch. |
401 from FHIR calls |
Missing/expired token, or the token's granted scopes do not cover the resource and interaction. |
| Waiting page never advances | You opened the app directly — start from a patient chart inside the EMR instead. |
Choose launcher locations
In an app's launch configuration, select Chart Launch, Consult Launch, or both. Existing apps default to Chart Launch. Save the configuration, then update the sandbox deployment to send the versioned settings through the outbox. Production location changes are part of a new security review snapshot.
Chart Launch appears in the chart's bottom toolbar SMART App Launcher. Consult Launch appears in the Consults header and inside an existing consult. Launching from the header creates an Incomplete consult and tracking task; launching inside a consult reuses it. A saved SMART app link supports later launches for that consult. The app opens in a new tab; refresh the chart on return to see a newly created consult.
The consult is the FHIR ServiceRequest, while its request and response PDFs
are DocumentReference resources linked through context.related. The saved
SMART launcher link is local navigation metadata. Neither its ID nor a document
ID should be used as the ServiceRequest ID. Existing FHIR logical IDs and consult
UUID identifiers are unchanged.
Use the cached permissions catalog to request launch/servicerequest and
user/ServiceRequest.r for referral context and reads. Document uploads separately
require user/DocumentReference.c. The token response supplies the referral as
fhirContext: [{ "reference": "ServiceRequest/{id}" }]; the browser launch URL
continues to contain only iss and the opaque launch handle. Selecting a
launcher location grants no additional FHIR access.