Errors

Per-status walkthrough. CollectiveX Health's partner API returns three different error envelope shapes depending on where in the pipeline the error was raised.

Three envelope shapes

Flat envelope (422, 500, 502)

Origin-side errors with context:

{
  "request_id": "550e8400-e29b-71d4-a716-446655440000",
  "trace_id": "01HQRS4A8NZX2K3V5W7Y9B1M3P",
  "error": {
    "code": "<machine-readable-code>",
    "message": "<human-readable-string>",
    "details": null
  }
}

request_id is echoed from your request. trace_id is CxH-generated — include it in support tickets. details may be null or an object with field-level context. All 422 responses use this shape, including Pydantic schema-validation failures (error.code = "invalid_request") and clinical-query refusals (error.code = "out_of_scope").

Gateway envelope — 401 (Zuplo problem+json)

Zuplo edge auth rejections use RFC 7807 problem+json:

{
  "type": "https://httpproblems.com/http-status/401",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authorization Failed",
  "instance": "/v1/oura/recommendation",
  "trace": {
    "timestamp": "2026-04-28T15:20:11.482Z",
    "requestId": "<zuplo-request-id>",
    "rayId": "<cf-ray-id>"
  }
}

detail is one of: "Authorization Failed" (key invalid/revoked/wrong-scheme), "No Authorization Header" (header missing), or "Invalid Authorization Scheme" (scheme other than ApiKey). trace.requestId is correlatable — include it in support tickets.

Origin consent rejections use the simpler shape (the consent gate runs before the body is fully parsed):

{ "detail": "consent_not_provisioned" }

detail is one of consent_not_provisioned (no consent record exists for this tenant — provisioning hasn't happened yet) or consent_revoked (an active consent record was revoked). No request_id, no trace_id — this rejection happens upstream of body parsing.

Zuplo 429 envelope

Rate-limit 429 is Zuplo-emitted, not CxH-emitted. The body is Zuplo's own format (not the flat envelope). The Retry-After HTTP header is authoritative — use that to drive your backoff. See rate-limits.md.

Per-status reference

400 Bad Request

Invalid JSON, missing Content-Type: application/json, or unparseable body.

{ "detail": "Expecting value: line 1 column 1 (char 0)" }

Fix: Validate JSON syntax before sending. Most HTTP clients catch this client-side. Retry: Do not retry without fixing the body.

401 Unauthorized

Gateway-level auth failure (Zuplo problem+json envelope).

{
  "type": "https://httpproblems.com/http-status/401",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authorization Failed",
  "instance": "/v1/oura/recommendation",
  "trace": {
    "timestamp": "2026-04-28T15:20:11.482Z",
    "requestId": "<zuplo-request-id>",
    "rayId": "<cf-ray-id>"
  }
}

detail strings you may see: - "No Authorization Header" — header is missing entirely. - "Invalid Authorization Scheme" — scheme other than ApiKey. Bearer is rejected; only ApiKey is accepted (see authentication.md). - "Authorization Failed" — key is malformed, revoked, or scoped to a different environment (sandbox key against prod, or vice versa).

trace.requestId is the Zuplo-side correlation id — include it in support tickets.

Fix: Verify your Authorization: ApiKey $CXH_API_KEY header is intact and uses ApiKey (not Bearer). Check key rotation status with support. Retry: Do not retry until the key is fixed.

403 Forbidden (gateway)

Key is valid but not entitled for the route. Zuplo problem+json envelope (same shape as 401 above).

{
  "type": "https://httpproblems.com/http-status/403",
  "title": "Forbidden",
  "status": 403,
  "detail": "missing_partner_id",
  "instance": "/v1/oura/recommendation",
  "trace": { "timestamp": "...", "requestId": "...", "rayId": "..." }
}

Unusual in normal operation — Zuplo injects X-Zuplo-Partner-Id for you based on the API key. Seeing this means either:

Fix: Contact support with trace.requestId from your client logs. Retry: Do not retry.

Partner-level consent is missing or has been withdrawn.

Two distinct detail values:

{ "detail": "consent_not_provisioned" }

No active consent record exists for your tenant. This is the day-zero state — your API key was issued but consent provisioning is a separate manual step performed by CollectiveX. If you see this on your first request, your tenant just hasn't been provisioned yet — reply to whoever delivered your key.

{ "detail": "consent_revoked" }

A previously-active consent record has been revoked — either at your request, by CxH, or by regulatory mandate. This is a contract-level event, not a day-zero state.

Fix: - consent_not_provisioned → reply to your key delivery email asking for consent activation. - consent_revoked → contact support; this is a contract-level issue.

Retry: Do not retry until consent is provisioned/reinstated.

422 Unprocessable Entity — invalid_request

{
  "request_id": "...",
  "trace_id": "...",
  "error": {
    "code": "invalid_request",
    "message": "body.user_context.cycle_phase: invalid value 'not_applicable'. Must be one of: follicular, luteal, menstrual, ovulatory.",
    "details": null
  }
}

Pydantic validation failure. The message names the field path and the exact mismatch. Common causes:

Fix: Read the message field. All field-level errors are self-explanatory. Retry: Do not retry without fixing the body. Don't reuse the same request_id on a retry — invalid_request responses aren't cached, so a new attempt with the same id starts fresh; but for hygiene, mint a new id per attempt.

422 Unprocessable Entity — out_of_scope

{
  "request_id": "...",
  "trace_id": "...",
  "error": {
    "code": "out_of_scope",
    "message": "Query appears clinical. CXH does not provide diagnostic or prescriptive advice.",
    "details": null
  }
}

Fired only when safety_mode: "strict" AND the query matches internal clinical keyword patterns (diagnostic terminology, prescription references, symptom descriptions). The engine refuses to answer.

Fix: Two options: 1. If your integration is for wellness (not clinical), use safety_mode: "permissive" — clinical framing in responses is still suppressed, but requests aren't rejected outright. 2. If the query is genuinely clinical, your integration should route it to a different surface — e.g. "this question is outside what we answer here; please consult a clinician."

Retry: Do not retry the same query. Reformulate or reroute.

429 Too Many Requests

Rate limit: 30 req/min per partner API key.

HTTP/1.1 429 Too Many Requests
Retry-After: 17
Content-Type: application/json

<Zuplo-emitted rate-limit body>

The body shape is Zuplo-emitted and covered by the auto-generated API reference at partnerdocs.collectivex.health/api. The Retry-After header value (seconds) is the authoritative wait time.

Fix: Exponential backoff with jitter. See rate-limits.md for a code example. Retry: Yes, after Retry-After seconds. Reuse the same request_id — idempotency will serve the cached result if your earlier request had already succeeded before the rate limit kicked in.

500 Internal Server Error

{
  "request_id": "...",
  "trace_id": "...",
  "error": {
    "code": "internal_error",
    "message": "An unexpected error occurred. Retry in a few seconds.",
    "details": null
  }
}

Unexpected origin-side failure. CxH monitors these and pages on-call. The trace_id is the correlation ID — include it in any support ticket.

Fix: Nothing partner-side. Retry: Yes, after 1–2s. Exponential backoff if it persists.

502 Bad Gateway

{
  "request_id": "...",
  "trace_id": "...",
  "error": {
    "code": "persistence_failed",
    "message": "Upstream audit-trail write failed. Retry after 1s.",
    "details": null
  }
}

The recommendation was generated successfully but the audit-trail write to our oura_payloads collection failed. We fail closed — we won't serve a response we can't persist, because audit-trail completeness is a GDPR Art. 9 obligation.

Fix: Nothing partner-side. Retry: Yes, after 1s. Reuse the same request_id — if the write succeeded on retry, idempotency will serve the cached response.

503 Service Unavailable

Transient infrastructure failure (cold-start timeout, upstream dependency outage). Body is FastAPI-default {"detail": "..."}.

Fix: Nothing partner-side. Retry: Yes, exponential backoff starting at 1s, max 30s.

Retry guidance summary

Status Retry? Backoff Reuse request_id?
400
401 ❌ (fix key first)
403 (gateway) ❌ (contact support)
403 (consent) ❌ (contact support)
422 invalid_request ❌ (fix body)
422 out_of_scope ❌ (reformulate/reroute)
429 Honor Retry-After
500 1s, 2s, 4s, 8s (max 30s)
502 1s, then 2s, 4s (max 3 retries)
503 1s, 2s, 4s, 8s (max 30s)

When you're stuck

When contacting support, include whichever id is present.