Idempotency

Every POST /v1/oura/recommendation is idempotent on the (client_id, request_id) pair for 5 minutes.

TL;DR

How the cache key is computed

cache_key = hash(client_id + ":" + request_id)
TTL       = 300 seconds
storage   = Redis (origin-side, EU region)

When the cache serves a replay

A repeat request hits the cache iff all three hold:

  1. Same client_id (automatic — derived from your API key).
  2. Same request_id in the body.
  3. Less than 5 minutes since the original request was served.

Cache hit: response carries X-CxH-Cache: hit and served_at reflects the original processing time, not the replay time.

Cache miss: response carries X-CxH-Cache: miss and the request is processed from scratch.

You can therefore drive cache-aware logic off the header rather than parsing served_at deltas:

r = httpx.post(...)
if r.headers.get("X-CxH-Cache") == "hit":
    # idempotent replay — same recommendation as before
    ...

When the cache does NOT serve a replay

UUID version recommendations

Choosing your retry strategy

Scenario A: client-side timeout (no response from us)

Your client sent the request but timed out before our response reached you. Unclear whether we processed it.

Scenario B: 429 rate-limited

You hit 30 req/min. Zuplo rejected before the origin saw the request.

Scenario C: 502 persistence_failed

We generated a recommendation but couldn't persist it to the audit-trail. We fail-closed, so we didn't serve the response.

Scenario D: 500 internal_error

Unexpected failure somewhere in the pipeline.

Anti-patterns

Audit-trail implication

Every served response (cache hit or miss) corresponds to exactly one row in our oura_payloads collection. The row is written on the cache miss that produced the original response. Cache hits do not write new rows — they serve what's already stored.

This matters for:

Example: safe retry loop

import time
import uuid
import httpx

def request_recommendation(body: dict, *, max_retries: int = 3) -> dict:
    request_id = str(uuid.uuid4())  # or uuid7 if you have it
    body = {**body, "request_id": request_id}

    for attempt in range(max_retries + 1):
        try:
            r = httpx.post(
                f"{CXH_BASE_URL}/v1/oura/recommendation",
                headers={"Authorization": f"ApiKey {CXH_API_KEY}"},
                json=body,
                timeout=30.0,
            )
        except httpx.TimeoutException:
            if attempt < max_retries:
                time.sleep(2 ** attempt)  # 1s, 2s, 4s
                continue
            raise

        if r.status_code == 429:
            retry_after = int(r.headers.get("Retry-After", 1))
            time.sleep(retry_after)
            continue
        if r.status_code in (500, 502, 503) and attempt < max_retries:
            time.sleep(2 ** attempt)
            continue

        r.raise_for_status()
        return r.json()

    raise RuntimeError(f"Exceeded {max_retries} retries for request_id={request_id}")

Note the request_id is generated once, outside the retry loop. Every retry reuses it. This is the correct pattern.