Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no cardYour first API call in 2 minutes →
Oura API to FHIR: Oura sleep, HRV and heart rate in your app
Read sleep, HRV, heart rate, SpO₂ and steps from the Oura API v2 and store them as LOINC-coded FHIR R4 Observations, with curl, Python and JavaScript; access, OAuth, endpoints and limits.
The Oura API v2 gives your app a person's sleep, heart rate, HRV, SpO₂ and activity after they approve your OAuth app. You can store each reading as a LOINC-coded FHIR R4 Observation with one standard API call. Anpheros does not connect to Oura's cloud itself. Your code reads Oura, and Anpheros keeps the record with the person's consent. On phones, the Oura app also writes to Apple Health and Health Connect, so the same data can reach you through HealthKit or Health Connect.
Read Oura sleep and store it in FHIR (5 minutes)
First, the night's sleep from Oura (an access token from your Oura OAuth app; Oura asks for a User-Agent):
curl -H "Authorization: Bearer $OURA_TOKEN" \
-H "User-Agent: YourApp/1.0" \
"https://api.ouraring.com/v2/usercollection/sleep?start_date=2026-11-02&end_date=2026-11-03"
Then the sleep duration as a FHIR Observation in the person's record (sandbox key sk_test_…):
curl -X POST https://platform.anpheros.com/fhir/R4/Observation \
-H "Authorization: Bearer $ANPHEROS_KEY" \
-H "Content-Type: application/fhir+json" \
-H "X-Anpheros-Author-Type: device" \
-d '{"resourceType":"Observation","status":"final",
"category":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/observation-category","code":"activity"}]}],
"code":{"coding":[{"system":"http://loinc.org","code":"93832-4","display":"Sleep duration"}]},
"subject":{"reference":"Patient/<id>"},
"effectivePeriod":{"start":"2026-11-02T23:10:00+01:00","end":"2026-11-03T06:52:00+01:00"},
"valueQuantity":{"value":431,"unit":"min","system":"http://unitsofmeasure.org","code":"min"},
"device":{"display":"Oura Ring"}}'
The same in Python: every sleep period becomes one Observation, sent together in one transaction Bundle.
import os, requests
OURA = "https://api.ouraring.com/v2/usercollection"
FHIR = "https://platform.anpheros.com/fhir/R4"
oura = {"Authorization": f"Bearer {os.environ['OURA_TOKEN']}", "User-Agent": "YourApp/1.0"}
fhir = {"Authorization": f"Bearer {os.environ['ANPHEROS_KEY']}", "Content-Type": "application/fhir+json"}
sleep = requests.get(f"{OURA}/sleep", headers=oura,
params={"start_date": "2026-11-01", "end_date": "2026-11-03"}).json()["data"]
entries = [{
"request": {"method": "POST", "url": "Observation"},
"resource": {
"resourceType": "Observation", "status": "final",
"category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "activity"}]}],
"code": {"coding": [{"system": "http://loinc.org", "code": "93832-4", "display": "Sleep duration"}]},
"subject": {"reference": "Patient/<id>"},
"effectivePeriod": {"start": s["bedtime_start"], "end": s["bedtime_end"]},
"valueQuantity": {"value": round(s["total_sleep_duration"] / 60), "unit": "min",
"system": "http://unitsofmeasure.org", "code": "min"},
"device": {"display": "Oura Ring"},
},
} for s in sleep if s.get("total_sleep_duration")]
r = requests.post(FHIR, headers=fhir, json={"resourceType": "Bundle", "type": "transaction", "entry": entries})
print(r.status_code, len(entries), "nights saved")
And in JavaScript (Node 18+), steps per day from daily_activity:
const oura = { Authorization: `Bearer ${process.env.OURA_TOKEN}`, 'User-Agent': 'YourApp/1.0' };
const fhir = { Authorization: `Bearer ${process.env.ANPHEROS_KEY}`, 'Content-Type': 'application/fhir+json' };
const { data } = await (await fetch(
'https://api.ouraring.com/v2/usercollection/daily_activity?start_date=2026-11-01&end_date=2026-11-03',
{ headers: oura })).json();
for (const day of data) {
await fetch('https://platform.anpheros.com/fhir/R4/Observation', {
method: 'POST', headers: fhir,
body: JSON.stringify({
resourceType: 'Observation', status: 'final',
category: [{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'activity' }] }],
code: { coding: [{ system: 'http://loinc.org', code: '41950-7', display: 'Number of steps in 24 hour Measured' }] },
subject: { reference: 'Patient/<id>' },
effectivePeriod: { start: day.day, end: day.day },
valueQuantity: { value: day.steps, unit: '{steps}', system: 'http://unitsofmeasure.org', code: '{steps}' },
device: { display: 'Oura Ring' },
}),
});
}
Field names (total_sleep_duration in seconds, bedtime_start, bedtime_end, steps) are from Oura's API v2 specification. The Anpheros side is standard FHIR R4, so the same requests work with any FHIR server.
Ways to get Oura data into FHIR
| Path | Status | What you need |
|---|---|---|
| Your backend reads the Oura API v2, then writes FHIR to Anpheros | Works today | An Oura OAuth app, the person's consent, an active Oura Membership |
| Oura app → Apple Health → your iPhone app reads HealthKit → Anpheros | Works today | HealthKit permissions. See Apple Health to FHIR |
| Oura app → Health Connect → your Android app → Anpheros | Works today | Health Connect permissions. Gen3 and later rings need a membership. See Health Connect to FHIR |
| A direct Oura connector inside Anpheros | On our roadmap | Not available yet. Use the button below to tell us you need it |
The Oura API v2 in short
Access. Register an API application in Oura's developer portal. A new application works for up to 10 users until Oura approves it; after approval there is no user limit. Each person approves every data type you ask for. When a person's Oura subscription has expired, the API answers 403. Personal access tokens stopped working in December 2025, so every integration now uses OAuth. API v1 was removed in January 2024.
Auth. OAuth 2.0 authorization code flow (server side), or a client-side implicit flow with a token of about 30 days and no refresh token. Authorize at https://cloud.ouraring.com/oauth/authorize, exchange the code at https://api.ouraring.com/oauth/token. Refresh tokens are single use: each refresh returns a new one. Scopes: email, personal, daily, heartrate, workout, tag, session, spo2, and heart_health in the newest specification.
| Data | Endpoint (GET, base https://api.ouraring.com) |
Fields you will use |
|---|---|---|
| Sleep periods | /v2/usercollection/sleep |
total_sleep_duration, stage durations, average_heart_rate, lowest_heart_rate, average_hrv, hrv, average_breath |
| Sleep score | /v2/usercollection/daily_sleep |
daily score and contributors |
| Heart rate | /v2/usercollection/heartrate |
timestamp, bpm, source (5-minute samples) |
| SpO₂ | /v2/usercollection/daily_spo2 |
spo2_percentage.average, breathing_disturbance_index |
| Steps and activity | /v2/usercollection/daily_activity |
steps, calories |
| Also | daily_readiness, daily_stress, daily_resilience, vO2_max, workout, session, tag |
HRV has no endpoint of its own: it comes inside sleep. Every endpoint has a sandbox twin under /v2/sandbox/usercollection/… with sample data. Webhooks (/v2/webhook/subscription) notify your server about 30 seconds after the ring syncs; Oura suggests one historical pull when a person connects, then webhooks. Rate limits apply per access token and per application, and a 429 comes with Retry-After and X-RateLimit-* headers. Sleep data appears after the person opens the Oura app.
Sources: Oura API v2 documentation and OpenAPI specification, authentication, November 2026.
Oura data as FHIR Observations
| Oura data | LOINC | UCUM unit | Note |
|---|---|---|---|
Sleep duration (total_sleep_duration, seconds) |
93832-4 | min |
divide by 60 |
| Deep, REM, light sleep | 93831-6, 93829-0, 93830-8 | min |
components or separate Observations |
Steps (daily_activity.steps) |
41950-7 | {steps} |
one per day |
Heart rate (heartrate.bpm) |
8867-4 | /min |
daily min, average and max rather than every sample |
| Lowest heart rate in sleep | 40443-4 | /min |
resting heart rate |
| SpO₂ average | 59408-5 | % |
|
HRV (average_hrv) |
see note | ms |
Oura reports a night-time average; do not file it under LOINC 80404-7 unless it is SDNN. See Wearable data in FHIR |
Frequently asked questions
Is the Oura API free?
Oura does not charge developers for the API. People need an active Oura Membership for their data to come through the API: when it has expired, requests return 403.
Can I still use an Oura personal access token?
No. Oura deprecated personal access tokens in December 2025. Create an OAuth application, even for your own data.
Where is HRV in the Oura API?
Inside the sleep periods (/v2/usercollection/sleep): average_hrv for the night and hrv as a series. There is no separate HRV endpoint.
How many users can my Oura app have?
Up to 10 until Oura approves the application; after approval, no limit.
Does Anpheros connect to Oura directly?
Not yet. Today your code reads the Oura API (or HealthKit / Health Connect on the phone) and writes standard FHIR to Anpheros. A direct Oura connector is on our roadmap; tell us with the button on this page if you need it.
What is the Oura API rate limit?
Oura limits each access token and each application and answers 429 with a Retry-After header. The current specification does not publish a fixed number, so read the X-RateLimit-* headers.
Related
- Wearable APIs: every brand compared
- Wearable data in FHIR (Apple Health, Health Connect)
- Apple Health (HealthKit) to FHIR
- Health Connect to FHIR
- Garmin API to FHIR · WHOOP API to FHIR · Withings API to FHIR
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/oura-api.md and help me apply this guide to my own project. 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 →