Oura Ring Integration
Oura Ring provides sleep, readiness, activity, and heart rate variability (HRV) data through the Oura Cloud API v2.
OAuth Setup
Prerequisites
- Create an Oura developer account at cloud.ouraring.com/oauth/applications
- Register a new OAuth application
- Set the redirect URI to
https://my.loop.health/api/patient-graph/wearables/callback/oura
Environment Variables
OURA_CLIENT_ID=your-oura-client-id
OURA_CLIENT_SECRET=your-oura-secret
OURA_REDIRECT_URI=https://my.loop.health/api/patient-graph/wearables/callback/ouraOAuth Scopes
The following scopes are requested during authorization:
| Scope | Data Access |
|---|---|
email | User email |
personal | Profile info |
daily | Daily summaries (readiness, sleep, activity) |
heartrate | Heart rate and HRV data |
workout | Workout sessions |
session | Mindfulness/meditation sessions |
Connection Flow
Step 1: Initiate Connection
curl -X POST "https://my.loop.health/api/patient-graph/wearables/connect/oura" \
-H "Authorization: Bearer $CLERK_JWT"Response:
{
"success": true,
"data": {
"authUrl": "https://cloud.ouraring.com/oauth/authorize?client_id=...&redirect_uri=...&scope=email+personal+daily+heartrate+workout+session&response_type=code&state=..."
}
}The user is redirected to this URL to authorize Loop Health.
Step 2: OAuth Callback
After authorization, Oura redirects to the callback URL:
GET /api/patient-graph/wearables/callback/oura?code=AUTH_CODE&state=STATE_TOKENThe server exchanges the authorization code for access and refresh tokens, which are stored in the Patient Graph.
Step 3: Initial Sync
curl -X POST "https://my.loop.health/api/patient-graph/wearables/sync" \
-H "Authorization: Bearer $CLERK_JWT" \
-H "Content-Type: application/json" \
-d '{ "source": "oura" }'Data Collected
Daily Readiness
{
"metricType": "readiness",
"source": "oura",
"metrics": {
"score": 82,
"temperatureDeviation": -0.1,
"temperatureTrendDeviation": 0.02,
"activityBalance": 75,
"bodyTemperature": 36.8,
"hrvBalance": 80,
"previousDayActivity": 70,
"previousNight": 85,
"recoveryIndex": 78,
"restingHeartRate": 58,
"sleepBalance": 88
}
}Daily Sleep
{
"metricType": "sleep",
"source": "oura",
"metrics": {
"score": 85,
"totalSleep": 28800,
"efficiency": 92,
"latency": 420,
"remSleep": 5400,
"deepSleep": 6300,
"lightSleep": 17100,
"awake": 1800,
"restfulness": 80,
"timing": 90,
"bedtimeStart": "2024-06-14T22:30:00Z",
"bedtimeEnd": "2024-06-15T06:30:00Z"
}
}Daily Activity
{
"metricType": "activity",
"source": "oura",
"metrics": {
"score": 78,
"activeCalories": 450,
"totalCalories": 2200,
"steps": 8500,
"equivalentWalkingDistance": 6800,
"inactivityAlerts": 2,
"meetDailyTargets": 1,
"moveEveryHour": 8,
"trainingFrequency": 3,
"trainingVolume": 2
}
}Heart Rate Variability
{
"metricType": "hrv",
"source": "oura",
"metrics": {
"averageHrv": 45,
"maxHrv": 85,
"minHrv": 18,
"restingHeartRate": 58
}
}Automatic Sync
A Trigger.dev job (syncWearablesDaily) runs daily at 3:00 AM UTC to fetch the latest Oura data:
- Finds all users with active Oura connections
- Refreshes OAuth tokens if expired
- Fetches daily readiness, sleep, activity, and HRV from Oura API
- Normalizes data and stores in
patient_graph.wearable_data - Updates daily stats in
patient_wearable_daily_stats - Records
wearable_syncevent in patient events
Troubleshooting
”Token expired” errors
OAuth tokens are automatically refreshed during sync. If refresh fails, the user needs to reconnect via the Connect flow.
Missing data
Oura may not have data for days when the ring wasn’t worn. The sync job gracefully handles missing data points.
Rate limits
The Oura API has rate limits. The sync job processes users sequentially with delays between requests to avoid hitting limits.