Python client for the Oura API v2 with OAuth2 support.
Oura is a wearable ring for monitoring sleep, activity, and workouts.
- 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
The oura_ring module can be installed via pip:
pip install oura-ring
Or, using uv:
uv add oura-ring
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.
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.
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.
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:
...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)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.
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"
}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"
},
...
]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"
},
...
]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"
},
...
]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"
},
...
]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
},
...
]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
},
...
]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"
},
...
]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"
},
...
]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"
},
...
]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"
},
...
]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"
},
...
]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
},
...
]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"
},
...
]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"
}
},
...
]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
}
},
...
]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"
},
...
]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"
]
},
...
]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"
},
...
]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]