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:

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:

  1. Platform floor (CxH-side, non-configurable) — minimum confidence required for CxH to assert a wellness recommendation at all.
  2. Client floor (minimum_confidence on 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:

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