Skip to Content
WearablesOura Ring

Oura Ring Integration

Oura Ring provides sleep, readiness, activity, and heart rate variability (HRV) data through the Oura Cloud API v2.

OAuth Setup

Prerequisites

  1. Create an Oura developer account at cloud.ouraring.com/oauth/applications 
  2. Register a new OAuth application
  3. 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/oura

OAuth Scopes

The following scopes are requested during authorization:

ScopeData Access
emailUser email
personalProfile info
dailyDaily summaries (readiness, sleep, activity)
heartrateHeart rate and HRV data
workoutWorkout sessions
sessionMindfulness/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_TOKEN

The 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:

  1. Finds all users with active Oura connections
  2. Refreshes OAuth tokens if expired
  3. Fetches daily readiness, sleep, activity, and HRV from Oura API
  4. Normalizes data and stores in patient_graph.wearable_data
  5. Updates daily stats in patient_wearable_daily_stats
  6. Records wearable_sync event 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.