Tools for acquiring and analyzing Oura API data.
Project description
Oura Ring for Python
Tools for acquiring and analyzing Oura API data.
Oura is a wearable ring for monitoring sleep, activity, and workouts.
Contents
- Installation
- Getting Started
- API Requests
- Get Personal Info
- Get Daily Sleep
- Get Daily SpO2
- Get Daily Stress
- Get Daily Activity
- Get Daily Readiness
- Get Daily Resilience
- Get Daily Cardiovascular Age
- Get VO2 Max
- Get Enhanced Tag
- Get Heart Rate
- Get Ring Battery Level
- Get Ring Configuration
- Get Rest Mode Period
- Get Sleep Periods
- Get Sleep Time
- Get Sessions
- Get Tags
- Get Workouts
- Usage With DataFrame
Installation
The oura_ring module can be installed via pip:
pip install oura-ring
Or, using uv:
uv add oura-ring
Getting Started
Note: Personal access tokens were deprecated by Oura in December 2025 — new ones can no longer be created, though previously-issued tokens may still work. New integrations must use OAuth2.
OAuth2 (recommended)
Create an application in Oura's developer portal to obtain a client_id and client_secret. Use OuraAuth to run the Authorization Code flow:
from oura_ring import OuraClient, OuraAuth
oauth = OuraAuth(client_id, client_secret)
# 1. Send the user to the authorization URL to grant access.
url = oauth.authorize_url(
redirect_uri="https://yourapp.com/callback",
state="<opaque CSRF value>",
)
# redirect the user to `url`...
# 2. Oura redirects back to your callback with a `?code=...` query param.
# Exchange that code for tokens.
tokens = oauth.exchange_code(code, redirect_uri="https://yourapp.com/callback")
# 3. Create a client with the access token.
client = OuraClient(access_token=tokens["access_token"])
When the access token expires, refresh it with the refresh token:
tokens = oauth.refresh_token(tokens["refresh_token"])
Important: Refresh tokens are single-use. The refresh response returns a new
refresh_tokenthat you must persist — the old one is invalidated, so the next refresh will fail if you don't store the new value.
Refreshing on demand
The client does not refresh automatically — it raises requests.HTTPError when a token has expired (HTTP 401). Catch that, refresh, persist the rotated tokens, and retry. A long-lived service looks roughly like this:
import requests
from oura_ring import OuraClient, OuraAuth
oauth = OuraAuth(client_id, client_secret)
def refresh_and_persist(tokens: dict) -> dict:
tokens = oauth.refresh_token(tokens["refresh_token"])
save_tokens(tokens) # your storage — both access_token AND the new refresh_token
return tokens
tokens = load_tokens() # your storage
try:
sleep = OuraClient(access_token=tokens["access_token"]).get_daily_sleep(...)
except requests.HTTPError as err:
if err.response.status_code != 401:
raise
tokens = refresh_and_persist(tokens)
sleep = OuraClient(access_token=tokens["access_token"]).get_daily_sleep(...)
Persisting the new refresh_token is the part that bites people: skip it and the next refresh fails with a 400, because Oura invalidated the old one the moment it issued the replacement.
Constructing a client from a token
OuraClient authenticates every request with a bearer token. That token can be an OAuth2 access token or a legacy personal access token, passed either by keyword or positionally:
from oura_ring import OuraClient
# OAuth2 access token (keyword)
client = OuraClient(access_token=token)
# Positionally — a legacy personal access token still works here for
# backwards compatibility.
client = OuraClient(token)
# Using a context manager
with OuraClient(token) as client:
...
Loading a token from the environment
It is best practice to store the token in a .env file. The value can be an OAuth2 access token or a legacy personal access token:
# Oura credentials
OURA_ACCESS_TOKEN="<ACCESS_TOKEN>"
You can use python-dotenv to load the environment variables for use in code:
import os
from dotenv import load_dotenv
load_dotenv()
token = os.getenv("OURA_ACCESS_TOKEN") or ""
client = OuraClient(token)
API Requests
There are 19 different API requests that OuraClient can make. Full Oura API v2 documentation can be found on Oura's website.
Scope and limitations (by design): the client returns the API's JSON as-is and raises on HTTP errors (including expired tokens and
429rate limits) — there is no automatic token refresh or retry built in; useOuraAuth.refresh_tokento rotate tokens. The optionalfields(sparse fieldsets) query param and the webhook subscription API are intentionally not exposed.
Get Personal Info
Method: get_personal_info()
Payload: None
Example Response:
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"age": 31,
"weight": 74.8,
"height": 1.8,
"biological_sex": "male",
"email": "example@example.com"
}
Get Daily Sleep
Method: get_daily_sleep(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"contributors": {
"deep_sleep": 57,
"efficiency": 98,
"latency": 81,
"rem_sleep": 20,
"restfulness": 54,
"timing": 84,
"total_sleep": 60
},
"day": "2022-07-14",
"score": 63,
"timestamp": "2022-07-14T00:00:00+00:00"
},
...
]
Get Daily Activity
Method: get_daily_activity(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"class_5_min": "<long sequence of 0|1|2|3|4|5>",
"score": 82,
"active_calories": 1222,
"average_met_minutes": 1.90625,
"contributors": {
"meet_daily_targets": 43,
"move_every_hour": 100,
"recovery_time": 100,
"stay_active": 98,
"training_frequency": 71,
"training_volume": 98
},
"equivalent_walking_distance": 20122,
"high_activity_met_minutes": 444,
"high_activity_time": 3000,
"inactivity_alerts": 0,
"low_activity_met_minutes": 117,
"low_activity_time": 10020,
"medium_activity_met_minutes": 391,
"medium_activity_time": 6060,
"met": {
"interval": 60,
"items": [
0.1,
...
],
"timestamp": "2021-11-26T04:00:00.000-08:00"
},
"meters_to_target": -16200,
"non_wear_time": 27480,
"resting_time": 18840,
"sedentary_met_minutes": 10,
"sedentary_time": 21000,
"steps": 18430,
"target_calories": 350,
"target_meters": 7000,
"total_calories": 3446,
"day": "2021-11-26",
"timestamp": "2021-11-26T04:00:00-08:00"
},
...
]
Get Daily Readiness
Method: get_daily_readiness(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"contributors": {
"activity_balance": 56,
"body_temperature": 98,
"hrv_balance": 75,
"previous_day_activity": None,
"previous_night": 35,
"recovery_index": 47,
"resting_heart_rate": 94,
"sleep_balance": 73
},
"day": "2021-10-27",
"score": 66,
"temperature_deviation": -0.2,
"temperature_trend_deviation": 0.1,
"timestamp": "2021-10-27T00:00:00+00:00"
},
...
]
Get Daily Resilience
Method: get_daily_resilience(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2021-10-27",
"contributors": {
"sleep_recovery": 64.2,
"daytime_recovery": 56.8,
"stress": 40.1
},
"level": "solid"
},
...
]
Get Daily Cardiovascular Age
Method: get_daily_cardiovascular_age(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2021-10-27",
"vascular_age": 32,
"pulse_wave_velocity": 7.2
},
...
]
Get VO2 Max
Method: get_vo2_max(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2021-10-27",
"timestamp": "2021-10-27T00:00:00+00:00",
"vo2_max": 48
},
...
]
Get Enhanced Tag
Method: get_enhanced_tag(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"tag_type_code": "string",
"start_time": "2019-08-24T14:15:22Z",
"end_time": "2019-08-24T14:15:22Z",
"start_day": "2019-08-24",
"end_day": "2019-08-24",
"comment": "string"
},
...
]
Get Heart Rate
Method: get_heart_rate(start_datetime: str = <end_datetime - 1 day>, end_datetime: str = <now>, latest: bool | None = None)
Payload:
start_datetime: The earliest datetime for which to get data. Expected in ISO 8601 format (YYYY-MM-DDThh:mm:ss). Defaults to one day before theend_datetimeparameter.end_datetime: The latest datetime for which to get data. Expected in ISO 8601 format (YYYY-MM-DDThh:mm:ss). Defaults to now.latest: If true, request only the most recent sample.
Example Response:
[
{
"bpm": 60,
"source": "sleep",
"timestamp_unix": 1609462923,
"timestamp": "2021-01-01T01:02:03+00:00"
},
...
]
Get Ring Battery Level
Method: get_ring_battery_level(start_datetime: str = <end_datetime - 1 day>, end_datetime: str = <now>, latest: bool | None = None)
Payload:
start_datetime: The earliest datetime for which to get data. Expected in ISO 8601 format (YYYY-MM-DDThh:mm:ss). Defaults to one day before theend_datetimeparameter.end_datetime: The latest datetime for which to get data. Expected in ISO 8601 format (YYYY-MM-DDThh:mm:ss). Defaults to now.latest: If true, request only the most recent sample.
Example Response:
[
{
"level": 87,
"charging": False,
"in_charger": False,
"timestamp_unix": 1609462923,
"timestamp": "2021-01-01T01:02:03+00:00"
},
...
]
Get Sleep Periods
Method: get_sleep_periods(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"average_breath": 12.625,
"average_heart_rate": 4.25,
"average_hrv": 117,
"awake_time": 4800,
"bedtime_end": "2022-07-12T09:25:14-07:00",
"bedtime_start": "2022-07-12T01:05:14-07:00",
"day": "2022-07-12",
"deep_sleep_duration": 4170,
"efficiency": 84,
"heart_rate": {
"interval": 300,
"items": [
None,
50,
46,
...
],
"timestamp": "2022-07-12T01:05:14.000-07:00"
},
"hrv": {
"interval": 300,
"items": [
None,
-102,
-122,
...
],
"timestamp": "2022-07-12T01:05:14.000-07:00"
},
"latency": 540,
"light_sleep_duration": 18750,
"low_battery_alert": False,
"lowest_heart_rate": 48,
"movement_30_sec": "<long sequence of 1|2|3>",
"period": 0,
"readiness_score_delta": 0,
"rem_sleep_duration": 2280,
"restless_periods": 415,
"sleep_phase_5_min": "<long sequence of 1|2|3|4>",
"sleep_score_delta": 0,
"time_in_bed": 30000,
"total_sleep_duration": None,
"type": "long_sleep"
},
...
]
Get Sleep Time
Method: get_sleep_time(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2019-08-24",
"optimal_bedtime": {
"day_tz": 0,
"end_offset": 0,
"start_offset": 0
},
"recommendation": "improve_efficiency",
"status": "not_enough_nights"
},
...
]
Get Ring Configuration
Method: get_ring_configuration(document_id: str | None = None)
Payload:
document_id: Optional individual document ID from a prior response. If omitted, all ring configuration documents are returned.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"color": "glossy_black",
"design": "heritage",
"firmware_version": "string",
"hardware_type": "gen1",
"set_up_at": "2019-08-24T14:15:22Z",
"size": 0
},
...
]
Get Rest Mode Period
Method: get_rest_mode_period(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"end_day": "2019-08-24",
"end_time": "2019-08-24T14:15:22Z",
"episodes": [
{
"tags": [
"string"
],
"timestamp": "2019-08-24T14:15:22Z"
}
],
"start_day": "2019-08-24",
"start_time": "2019-08-24T14:15:22Z"
},
...
]
Get Sessions
Method: get_sessions(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2021-11-12",
"start_datetime": "2021-11-12T12:32:09-08:00",
"end_datetime": "2021-11-12T12:40:49-08:00",
"type": "rest",
"heart_rate": None,
"heart_rate_variability": None,
"mood": None,
"motion_count": {
"interval": 5,
"items": [
0
],
"timestamp": "2021-11-12T12:32:09.000-08:00"
}
},
...
]
Get Daily SpO2
Method: get_daily_spo2(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2019-08-24",
"spo2_percentage": {
"average": 0
}
},
...
]
Get Daily Stress
Method: get_daily_stress(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2019-08-24",
"stress_high": 0,
"recovery_high": 0,
"day_summary": "restored"
},
...
]
Get Tags
Method: get_tags(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"day": "2021-01-01",
"text": "Need coffee",
"timestamp": "2021-01-01T01:02:03-08:00",
"tags": [
"tag_generic_nocaffeine"
]
},
...
]
Get Workouts
Method: get_workouts(start_date: str = <end_date - 1 day>, end_date: str = <today's date>, document_id: str | None = None)
Payload:
start_date: The earliest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to one day before theend_dateparameter.end_date: The latest date for which to get data. Expected in ISO 8601 format (YYYY-MM-DD). Defaults to today's date.
Example Response:
[
{
"id": "8f9a5221-639e-4a85-81cb-4065ef23f979",
"activity": "cycling",
"calories": 300,
"day": "2021-01-01",
"distance": 13500.5,
"end_datetime": "2021-01-01T01:00:00.000000+00:00",
"intensity": "moderate",
"label": None,
"source": "manual",
"start_datetime": "2021-01-01T01:30:00.000000+00:00"
},
...
]
Usage With DataFrame
Using Oura API data with a Pandas DataFrame is very straightforward:
>>> import pandas as pd
>>> sleep = client.get_daily_sleep()
>>> pd.json_normalize(sleep)
day score timestamp contributors.deep_sleep \
0 2022-09-01 76 2022-09-01T00:00:00+00:00 99
1 2022-09-02 81 2022-09-02T00:00:00+00:00 100
contributors.efficiency contributors.latency contributors.rem_sleep \
0 90 99 79
1 88 75 95
contributors.restfulness contributors.timing contributors.total_sleep
0 55 15 85
1 56 28 96
[2 rows x 10 columns]
>>> readiness = client.get_daily_readiness()
>>> pd.json_normalize(readiness)
day score temperature_deviation temperature_trend_deviation \
0 2022-09-01 87 -0.09 0.24
1 2022-09-02 91 -0.03 0.11
timestamp contributors.activity_balance \
0 2022-09-01T00:00:00+00:00 80
1 2022-09-02T00:00:00+00:00 86
contributors.body_temperature contributors.hrv_balance \
0 100 84
1 100 85
contributors.previous_day_activity contributors.previous_night \
0 None 75
1 None 88
contributors.recovery_index contributors.resting_heart_rate \
0 100 100
1 94 98
contributors.sleep_balance
0 87
1 93
[2 rows x 13 columns]
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file oura_ring-1.0.0.tar.gz.
File metadata
- Download URL: oura_ring-1.0.0.tar.gz
- Upload date:
- Size: 63.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df91b94a43f5fdc6aeda6ebe475835f6b18931e8cb6c213fde28874333b1a91c
|
|
| MD5 |
eaf126f8ad7324ca0077d99098886111
|
|
| BLAKE2b-256 |
504c59f39d9b2293e7d543025e62918336636493f15a9891391f6b9bc860dff8
|
File details
Details for the file oura_ring-1.0.0-py3-none-any.whl.
File metadata
- Download URL: oura_ring-1.0.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3aad0b2ceb5ab31af59efc827644f772edc14680ed79b6691ac7f6bfc6459585
|
|
| MD5 |
b83735c0b41809f66c4c9434da0502bf
|
|
| BLAKE2b-256 |
98ae39bcc0f8d386485038469c747d09bd504a472933b668dc1f2ae2307d538c
|