Authentication
Every request to $CXH_BASE_URL/v1/oura/* MUST include an Authorization header with a CollectiveX Health partner API key.
Header format
Authorization: ApiKey zpka_<32+ alphanumeric chars>
ApiKey is the only accepted scheme. Bearer is rejected with 401.
Example:
curl -sS "$CXH_BASE_URL/v1/oura/recommendation" \
-H "Authorization: ApiKey $CXH_API_KEY" \
-H "Content-Type: application/json" \
-d '...'
How keys are issued
- Keys are minted by CollectiveX Health in the Zuplo dashboard. They never transit our source code, Secret Manager, or CI logs.
- You receive the key out-of-band (encrypted channel: 1Password share, PGP, or equivalent). Raw keys are never sent over email or Slack in plaintext.
- Each key is scoped to one partner tenant (e.g.
cxh-sandbox-oura,cxh-prod-oura). Sandbox keys never work against prod routes and vice versa. - The key embeds your tenant identifier internally. You do not send
X-Zuplo-Partner-Idyourself — the gateway injects it from the key.
Rotation
Standard rotation cadence is quarterly or on demand.
To request rotation:
- Contact support with your current tenant identifier and the reason (scheduled rotation / suspected compromise / developer churn).
- CollectiveX mints a new key and hands it over via the same out-of-band channel used for the original.
- You have a 24-hour overlap window where both the old and new keys are valid. Deploy the new key to all production callers within that window.
- The old key is revoked automatically at the end of the 24-hour window. CollectiveX notifies you once revocation is confirmed.
Revocation (emergency)
If you suspect a key has leaked (committed to a public repo, exposed in logs, lost laptop, departed team member with access):
- Contact support immediately with subject
URGENT: key revocation — <tenant-id>. - CollectiveX revokes the key within 15 minutes during business hours, or within 1 hour outside business hours.
- Revoked keys begin returning
401 Unauthorized(problem+json envelope per errors.md) within ~30 seconds of revocation. - A replacement key is issued within the same support response.
There is no self-service revocation endpoint for partner keys — this is deliberate. Zuplo dashboard access is CollectiveX-side only.
Why API keys instead of OAuth 2.0
Partner traffic is server-to-server, low-cardinality (one key per partner tenant), and trust-bootstrapped through a signed Data Processing Agreement. OAuth client-credentials flow adds a token-minting round-trip with no security benefit at our scale.
For end-user-facing integrations, CollectiveX has a separate Keycloak-based OIDC flow. That path is out of scope for /v1/oura/*.
What the gateway validates
Zuplo validates:
- Key presence — missing or malformed
Authorizationheader →401. The body is Zuplo's RFC 7807 problem+json envelope (see errors.md for the actual shape). - Key validity — key exists in Zuplo's key store and is not revoked →
401on failure. - Key-to-route scope — key is entitled to hit
/v1/oura/*→403on mismatch. - Rate limit — key is under 30 req/min →
429on breach. See rate-limits.md.
After validation, Zuplo injects the X-Zuplo-Partner-Id header and forwards the request to the CollectiveX origin. The origin then validates:
- Partner consent — active consent record exists for the partner tenant →
403on revocation. See errors.md forconsent_not_provisionedvsconsent_revoked.
All five checks happen before your request body is parsed. A valid request body with an invalid key never triggers clinical processing.
Key storage recommendations
- Store in a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, 1Password Service Accounts). Never in source, never in
.envfiles committed to git. - Inject at process start as an env var, not as a runtime config file.
- In CI, fetch at job start with
gcloud secrets versions access(or equivalent) and scrub fromset -xoutput with::add-mask::or equivalent. - If your stack supports it, prefer workload-identity-based secret access over long-lived service account keys.
Questions?
See support.md.