Log in Request access

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

  1. Ava EMR opens your launch URI with iss and launch parameters.
  2. You fetch the SMART discovery document from the issuer.
  3. You redirect the browser to the authorization endpoint with a PKCE challenge.
  4. The user approves your requested scopes.
  5. The EMR redirects back to your redirect URI with an authorization code.
  6. You exchange the code (plus your PKCE verifier) at the token endpoint.
  7. 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 follow iss values.
  • 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_challengeBASE64URL(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
  • scope must be a subset of the scopes in your app's deployed permission schema; launch is always required.
  • aud must be the FHIR issuer from step 1 — the EMR rejects mismatches.
  • redirect_uri must 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, not localhost).

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>"
}
  • scope is the set actually granted — it may be narrower than requested. Respect it.
  • patient carries the launch context: the id of the patient chart open in the EMR. Your patient/ scopes are confined to this patient.
  • id_token (present with openid) identifies the signed-in clinician; with fhirUser it includes a fhirUser claim pointing at their FHIR resource. Validate it against the issuer's jwks_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.