Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no cardYour first API call in 2 minutes →
Create an OAuth app and the consent screen
Step 3: register an OAuth application, send the person to the consent screen with PKCE and SMART v2 scopes, exchange the code for a 30-minute token, refresh and revoke, with copyable JavaScript and Python.
Step 3 of 5. An API key is for your server and your own test data. When a real person uses your app, the person signs in with Anpheros, sees a consent screen with your app's name, what it asks for and for how long, and approves it. Your app then gets a token for that one person's record, limited to what they approved. This is OAuth 2.1 with PKCE, in the SMART on FHIR format.
In short:
| Step | Call | Who |
|---|---|---|
| 1. Register the app | POST /admin/projects/{project_id}/applications |
you, once |
| 2. Send the person to consent | GET /oauth/authorize?… |
your app opens the browser |
| 3. The person approves | consent screen: scopes, purpose, 30/90/180/365 days | the person |
| 4. Exchange the code | POST /oauth/token |
your app |
| 5. Call the API | Authorization: Bearer at_… |
your app |
1. Register your app
In the dashboard, or with the Admin API and your dashboard user token (a Firebase ID token):
curl -X POST https://platform.anpheros.com/admin/projects/proj_…/applications \
-H "Authorization: Bearer $FIREBASE_ID_TOKEN" -H 'content-type: application/json' -d '{
"name": "CardioNotes",
"client_type": "public",
"redirect_uris": ["com.cardionotes.app:/oauth/callback", "http://localhost:5173/callback"],
"purpose": "Shows your blood pressure readings and lets you add new ones."
}'
{"id":"app_…","client_id":"client_…","client_type":"public","redirect_uris":["com.cardionotes.app:/oauth/callback","http://localhost:5173/callback"],"status":"active","…":"…"}
client_type:public(the default) for a mobile app, a single-page app or anything that cannot keep a secret;confidentialfor a backend. A confidential app gets aclient_secretonce, in this answer.redirect_uris: 1 to 10 addresses, eachhttps://…, a custom scheme for a mobile app, orhttp://localhost…for development. The address in the request must match one of them exactly.purpose(up to 500 characters) is shown to the person on the consent screen. Add a logo and a colour withPUT /admin/applications/{id}/branding.
2. Send the person to the consent screen
Create a PKCE pair and a state, keep both, and open the authorize address in the browser:
// in the browser or in Node 18+
const b64url = buf => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32))); // 43 characters
const challenge = b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)));
const state = crypto.randomUUID();
const url = new URL('https://platform.anpheros.com/oauth/authorize');
url.search = new URLSearchParams({
response_type: 'code',
client_id: 'client_…',
redirect_uri: 'http://localhost:5173/callback',
scope: 'patient/Observation.rs patient/Observation.c offline_access',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
}).toString();
location.href = url; // keep verifier and state until the person comes back
import base64, hashlib, secrets, urllib.parse
verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)
print("https://platform.anpheros.com/oauth/authorize?" + urllib.parse.urlencode({
"response_type": "code", "client_id": "client_…", "redirect_uri": "http://localhost:5173/callback",
"scope": "patient/Observation.rs patient/Observation.c offline_access",
"state": state, "code_challenge": challenge, "code_challenge_method": "S256"}))
Scopes use the SMART v2 format patient/<Type>.<cruds>: r read, s search, c create,
u update, d delete. Examples: patient/Observation.rs, patient/Condition.rs,
patient/*.rs (read everything), patient/Observation.rs?category=laboratory (lab results only).
Add offline_access for a refresh token and openid fhirUser for an id_token. An unknown scope
is refused with 400, not ignored. Discovery document:
https://platform.anpheros.com/.well-known/smart-configuration.
3. What the person sees
The consent screen shows your app's name, organization, logo and privacy link, the scopes in plain language, the purpose, whose record (their own or a family member's) and for how long: 30, 90, 180 or 365 days, 90 by default. In the sandbox the screen carries a "TEST · Sandbox" banner.
If the person approves, the browser goes to your redirect_uri with ?code=…&state=…. Check that
state is the one you sent. The code is valid for 10 minutes. If they decline, you get
?error=access_denied&state=….
4. Exchange the code for a token
curl -X POST https://platform.anpheros.com/oauth/token \
-d grant_type=authorization_code -d code="$CODE" -d code_verifier="$VERIFIER" \
-d redirect_uri=http://localhost:5173/callback -d client_id=client_…
{"access_token":"at_…","token_type":"Bearer","expires_in":1800,"scope":"patient/Observation.rs patient/Observation.c offline_access","patient":"…","grant_id":"…","refresh_token":"rt_…"}
- The access token lasts 30 minutes; the refresh token 30 days.
patientis the id of the person's record for your app: another app sees the same person under a different id.- A confidential app also sends
client_secret(or HTTP Basic authentication).
5. Call the API, refresh and revoke
curl "https://platform.anpheros.com/v1/patients/$PATIENT/observations?limit=10" -H "Authorization: Bearer $ACCESS_TOKEN"
# when the access token expires
curl -X POST https://platform.anpheros.com/oauth/token \
-d grant_type=refresh_token -d refresh_token="$REFRESH_TOKEN" -d client_id=client_…
# when the person signs out of your app
curl -X POST https://platform.anpheros.com/oauth/revoke -d token="$REFRESH_TOKEN" -d client_id=client_…
Refresh tokens rotate: each refresh returns a new one, and reusing an old one revokes the whole
chain (a sign that a token was stolen). The person can also revoke your app at any time from their
Connections in Anpheros; your next call then gets 401 The consent behind this token is no longer
active. Both SDKs implement this whole flow, including PKCE and refresh: see
Authentication and OAuth.
Common errors
| You get | Fix |
|---|---|
400 redirect_uri is not registered for this client. |
Register the exact redirect_uri, with the same scheme, host, port and path |
400 on an unknown scope |
Check the spelling: patient/Observation.rs, not patient/Observation.read |
invalid_grant Authorization code already used or expired. / PKCE verification failed. |
The code expired (10 minutes) or was already used, or the code_verifier is not the one behind the code_challenge |
403 The consent behind this token does not allow 'write' on Observation. |
Ask for patient/Observation.c in the scope |
Next step
Step 4: instead of asking every few minutes whether something changed, let the platform tell you: Receive your first webhook.
Frequently asked questions
Can a mobile app keep a client secret?
No. Register a mobile or browser app as public and use PKCE, which is required for every app. Only a backend should be confidential.
How long does the person's approval last?
As long as they chose on the consent screen: 30, 90, 180 or 365 days, 90 by default. They can withdraw it earlier at any time.
Do I need OAuth for my own test data?
No. For the sandbox and for data your own organization manages, use an API key on your server. OAuth is for acting on behalf of a person.
Ask your AI
Paste this into Claude, ChatGPT, Cursor or Claude Code. Your assistant reads this guide and adapts it to your code.
Read https://developers.anpheros.com/guides/oauth-app.md and help me register an OAuth app and run the consent flow with PKCE in my app. Use the Anpheros sandbox (base URL https://platform.anpheros.com) with my API key from the ANPHEROS_KEY environment variable, never written in the code. Give me complete, runnable code, the response I should expect, and how to fix the errors in https://developers.anpheros.com/guides/troubleshooting.md if a call fails.Open in ClaudeOpen in ChatGPTThis guide as MarkdownBuild the whole integration with an AI coding tool
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no cardYour first API call in 2 minutes →