Request / Response Reference
Full body contract for POST /v1/oura/recommendation.
Pure-metrics-only contract
We accept only raw scientific measurements. Proprietary aggregate scores — Oura's score, hrv_balance, activity_balance, sleep_balance, or any nested contributors.*_balance — are accepted at the wire (so SDK drift doesn't break requests) and silently discarded. They are not persisted, not read by the recommendation engine, and not returned in the response.
Why: peer-reviewed clinical literature grounds against raw scientific units (HRV rMSSD in ms, heart rate in bpm, sleep-stage durations in seconds, raw temperatures, raw SpO₂). Vendor 0–100 buckets are opaque proprietary transforms that differ across vendors and firmware versions. Building against raw metrics keeps the contract portable: the same body shape will apply when Whoop, Apple Health, or Garmin onboard.
Keep sending the raw fields. If your SDK surfaces aggregate scores in the same payload, leaving them in is fine — they land in a permissive-extras bucket and are dropped before storage.
Request body
Top-level fields
| Field | Type | Required | Notes |
|---|---|---|---|
request_id |
UUID v4/v7 string | ✅ | Idempotency key. See idempotency.md. Recommended: UUID v7. |
query |
string | ❌ | Free-text question. Omit for implicit-trigger (CxH auto-generates a relevant prompt from signals). |
client_trace_id |
string | ❌ | Your own trace correlation ID. Echoed back in telemetry, not in the response body. |
timezone |
IANA string | ✅ | E.g. "Europe/London", "America/Los_Angeles", "UTC". Abbreviations (EST, PST, CET) are rejected — see "Enum and range tables" below. |
safety_mode |
enum | ✅ | "strict" or "permissive". See table below. |
minimum_confidence |
float | ✅ | 0.0–1.0. Your per-request floor — responses below this confidence are suppressed with suppression_reason: "below_client_threshold". |
user_context |
object | ✅ | See "user_context" below. |
oura |
object | ✅ | See "oura payload" below. |
personal_info |
object | ❌ | Optional partner-provided user metadata. See "personal_info" below. |
user_context
| Field | Type | Required | Notes |
|---|---|---|---|
cycle_phase |
enum | ✅ | "menstrual" / "follicular" / "ovulatory" / "luteal". 4 values only. |
cycle_day |
int | ✅ | 1–40. Days since last period start. |
life_stage |
enum | ✅ | "reproductive" / "perimenopause" / "postmenopause". 3 values only. |
oura payload
At least one signal-bearing key must be populated. Signal-bearing keys:
sleep— array ofOuraSleepRecordheartrate— array ofOuraHeartRateBlockvo2_max— array ofOuraVo2MaxRecorddaily_readiness— array ofOuraDailyReadinessRecorddaily_sleep— array ofOuraDailySleepRecordworkout— array ofOuraWorkoutRecordsession— array ofOuraSessionRecord
Secondary (optional) keys: tag, enhanced_tag, rest_mode_period, ring_configuration, daily_cardiovascular_age, daily_stress, daily_resilience, daily_spo2.
The schema allows extra fields (model_config = {"extra": "allow"}). Any field not in the table — including Oura's aggregate score and contributors — is silently dropped.
Example oura.sleep[0] entry (pure metrics)
{
"day": "2026-04-23",
"bedtime_start": "2026-04-22T23:14:00+01:00",
"bedtime_end": "2026-04-23T07:32:00+01:00",
"total_sleep_duration": 27720,
"rem_sleep_duration": 5820,
"deep_sleep_duration": 4680,
"light_sleep_duration": 17220,
"awake_time": 1080,
"average_hrv": 48.5,
"average_heart_rate": 54,
"lowest_heart_rate": 48,
"average_breath": 14.8,
"temperature_deviation": -0.12
}
Example oura.daily_readiness[0] entry (post-ZUP-12 drop)
{
"day": "2026-04-23",
"timestamp": "2026-04-23T07:32:00+01:00"
}
Per ZUP-12 (schema change, shipped 2026-04-24), daily_readiness no longer models score or contributors fields. If your Oura SDK includes them, you may send them — they'll be dropped silently. The recommendation engine grounds on the raw metrics in sleep, heartrate, vo2_max, etc. instead.
personal_info (optional)
| Field | Type | Persisted? | Notes |
|---|---|---|---|
age_bucket |
string | ✅ | E.g. "25-34". Bucketed age; raw DOB/age is not accepted. |
email |
string | ❌ NOT persisted | Accepted at the wire, scrubbed before the audit-trail write to oura_payloads. Send it if your SDK forces you to; don't rely on it round-tripping. Do not use it as a lookup key. |
| Other partner-defined fields | any | mostly ✅ | PHI minimization drops the email field only; everything else currently persists. If you send raw DOB/SSN/etc., don't — we treat the partner as responsible for not sending PII we didn't request. |
Enum and range tables
| Field | Valid values |
|---|---|
user_context.cycle_phase |
menstrual, follicular, ovulatory, luteal |
user_context.life_stage |
reproductive, perimenopause, postmenopause |
user_context.cycle_day |
integer in [1, 40] inclusive |
safety_mode |
strict, permissive |
minimum_confidence |
float in [0.0, 1.0] inclusive |
timezone |
IANA region/city format (Europe/London, Australia/Sydney, America/Los_Angeles) OR the bare strings UTC / GMT. Abbreviations like EST, PST, CET, JST are rejected with 422 invalid_request — they're ambiguous across DST boundaries. |
safety_mode semantics
| Mode | Behavior |
|---|---|
strict |
Clinical-style queries (containing keywords matching internal diagnostic/prescriptive patterns) are rejected with 422 out_of_scope. Use when the integration is surfaced to consumers without a wellness disclaimer. |
permissive |
All wellness queries accepted. Clinical framing in the response is still suppressed by the recommendation engine — but the request is processed. Use for partner-internal research, product testing, or consumer surfaces that include the CxH wellness disclaimer. |
Response body
Success (200)
{
"request_id": "550e8400-e29b-71d4-a716-446655440000",
"recommendation": "Your HRV trend this week is trending healthy for your luteal phase. ...",
"citations": [
{
"citation_id": "CXH-01HQRS4A8NZX2K3V5W7Y9B1M3P",
"source_title": "Heart Rate Variability and the Menstrual Cycle: A Systematic Review",
"source_url": "https://doi.org/10.xxxx/xxxxx",
"relevance": 0.91
}
],
"recommendation_confidence": 0.78,
"suggested_questions": [
"How does my HRV compare to last week?",
"Should I adjust training intensity tonight?"
],
"trace_id": "01HQRS4A8NZX2K3V5W7Y9B1M3P",
"served_at": "2026-04-24T18:42:17.431Z",
"model_version": "cxh-oura-v1.3",
"suppression_reason": null,
"warnings": []
}
| Field | Type | Notes |
|---|---|---|
request_id |
string | Echoed from the request. |
recommendation |
string | null | 400–1500 chars when present. null when suppressed (see below). |
citations |
array | Up to 7 Citation objects. Empty when suppressed. |
recommendation_confidence |
float | 0.0–1.0. Always returned, even when suppressed. |
suggested_questions |
array | 0–5 follow-up prompts. Primary payload in Shape B (implicit-trigger) responses. |
trace_id |
string | Opaque CxH telemetry correlation ID. Include in support tickets. |
served_at |
ISO 8601 UTC | Response-emit timestamp. |
model_version |
string | Opaque version string. Changes on model/prompt updates — don't parse. |
suppression_reason |
string | null | See table below. null on successful recommendation. |
warnings |
array | Soft-default advisory signals. V1 code: unmapped_persona — persona fell outside the known table and was routed to a safe default. Non-breaking-additive field — expect new codes over time. |
Suppression
The recommendation engine has two confidence floors:
- Platform floor (CxH-side, non-configurable) — minimum confidence required for CxH to assert a wellness recommendation at all.
- Client floor (
minimum_confidenceon the request) — your per-request floor.
If recommendation_confidence is below either floor, the response is 200 OK with recommendation: null and one of:
suppression_reason |
When |
|---|---|
below_platform_threshold |
Below CxH-side platform floor. CxH decided we can't responsibly answer. |
below_client_threshold |
Above platform floor but below your request's minimum_confidence. Your filter, not ours. |
insufficient_signal |
Not enough Oura data (e.g. only one day of sleep, no HRV). Re-send with more oura.* history if available. |
Suppressed responses still return recommendation_confidence, suggested_questions, trace_id, served_at, model_version, and warnings. recommendation and citations are empty.
Shape A vs Shape B
The response shape is the same in both cases — what changes is which fields are primary for partner display:
- Shape A (explicit query,
queryfield present):recommendationis the primary payload.suggested_questionsare 0–5 optional follow-ups. - Shape B (implicit trigger,
queryomitted):suggested_questionsis the primary payload — these are CxH-generated prompts the user can tap.recommendationmay be null even when confidence is high (we don't proactively answer a question the user didn't ask).
Your integration decides which shape to use based on the UI surface.
Sample full request (Shape B / implicit trigger)
{
"request_id": "01HQRS4A8NZX2K3V5W7Y9B1M3P",
"timezone": "Europe/London",
"safety_mode": "permissive",
"minimum_confidence": 0.5,
"user_context": {
"cycle_phase": "follicular",
"cycle_day": 8,
"life_stage": "reproductive"
},
"oura": {
"sleep": [
{
"day": "2026-04-23",
"total_sleep_duration": 27720,
"rem_sleep_duration": 5820,
"deep_sleep_duration": 4680,
"average_hrv": 48.5,
"average_heart_rate": 54
}
],
"daily_readiness": [
{"day": "2026-04-23", "timestamp": "2026-04-23T07:32:00+01:00"}
],
"daily_sleep": [
{"day": "2026-04-23", "timestamp": "2026-04-23T07:32:00+01:00"}
]
},
"personal_info": {
"age_bucket": "25-34"
}
}
Note: no query field. The response will have suggested_questions populated and recommendation may be null — that's expected Shape B behavior.
What's not in this reference
- The formal JSON Schema is at docs.collectivex.health/api (auto-generated from the OpenAPI spec, kept in sync on every change). Use it for code generation.
- Error response shapes — see errors.md.
- Retry semantics — see idempotency.md.