Skip to main content

Python client for the Oura API v2 with OAuth2 support.

Project description

Oura Ring for Python

Python client for the Oura API v2 with OAuth2 support.

Oura is a wearable ring for monitoring sleep, activity, and workouts.

Contents

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_token that 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 429 rate limits) — there is no automatic token refresh or retry built in; use OuraAuth.refresh_token to rotate tokens. The optional fields (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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_datetime parameter.
  • 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 the end_datetime parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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 the end_date parameter.
  • 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

oura_ring-1.0.1.tar.gz (63.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oura_ring-1.0.1-py3-none-any.whl (18.0 kB view details)

Uploaded Python 3

File details

Details for the file oura_ring-1.0.1.tar.gz.

File metadata

  • Download URL: oura_ring-1.0.1.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

Hashes for oura_ring-1.0.1.tar.gz
Algorithm Hash digest
SHA256 03cb6381ee5b580b07f720f81052736e6aa96ed7e021158ae8fa4463d35d134f
MD5 7e7a11e9cddb8179ca8ea17a1467b3c5
BLAKE2b-256 65fe889cc769a5a32e03aad3d1d2309656af3984cf0c000d4aa5edad05a63ad1

See more details on using hashes here.

File details

Details for the file oura_ring-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: oura_ring-1.0.1-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

Hashes for oura_ring-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 43b52127d23b984b5d1ec75f2f72bd9abea395585f965637c3439daf6da5db6d
MD5 7707bc07a6763e8408d4923afb945c1f
BLAKE2b-256 95d597e6aae68241c02a7456bd7f5667a3701f4b6c44efd98cc6792c5bb9bfcd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page