Errors, Conventions & Reference
Health & status
| Endpoint | Auth | Purpose | Response |
|---|---|---|---|
GET /api/v1/health | Bearer | Verify connectivity + key validity (updates last_used_at) | { "status": "ok" } |
GET /api/v1/status | public | Service health incl. DB ping, for monitoring | { "status": "ok", "version": "…", "timestamp": "…" } |
GET /api/v1/openapi.json | public | Machine-readable spec (OpenAPI 3.0.3), Access-Control-Allow-Origin: * | OpenAPI JSON |
Error codes (complete)
All errors use { "success": false, "error": { "code", "message" } }; validation errors add error.fields: [{ "field": "measurements[0].axialLength", "code": "too_big" }] (path + zod issue code only, never received values — PII protection).
| Code | HTTP | Where |
|---|---|---|
INVALID_API_KEY | 401 | all authenticated endpoints |
VALIDATION_ERROR | 400 | import, imports/list, practice/mappings, setup/db-key … |
INVALID_REQUEST | 400 | JSON not parseable |
INVALID_CODE | 400 | setup/redeem |
INVALID_HOSTNAME | 400 | setup/redeem |
ALREADY_REDEEMED | 409 | setup/redeem |
CODE_EXPIRED | 410 | setup/redeem |
KEY_ALREADY_SET | 409 | setup/db-key POST (one-shot) |
KEY_NOT_FOUND | 404 | setup/db-key GET |
CONSENT_MISSING | 403 | import mode follow_up |
CONSENT_WITHDRAWN | 403 | import mode follow_up |
TOKEN_FOREIGN | 403 | import mode token (foreign practice) |
NOT_FOUND | 404 | import (patient/practice not found) |
RATE_LIMIT_EXCEEDED | 429 | all (with Retry-After) |
INTERNAL_ERROR | 500 | all |
Typical flows (sequences)
Onboarding
Portal: generate setup code (MK-XXXX-XXXX, expires_at, store code_hash)
Client → POST /setup/redeem { code } → { apiKey, host, encryptionKey }
Client → GET /health (Bearer) → { status: ok }
Client → POST /setup/db-key { encryptedDbKey } → { success: true } (optional, one-shot)
First import → linking → follow-up import
Client → POST /import { mode: first_visit, devicePseudonym, hints, measurements[] }
→ { unmatched: N, skip_pseudonyms, … } (lands in pending queue)
Portal: human links pseudonym → patient (mapping + persistentToken created)
Client → GET /practice/mappings?since=…
→ { mappings: [ { devicePseudonym, mkClientId, revoked:false } ], nextSince }
Client → POST /import { mode: token, persistentToken, measurements[] }
→ { assigned: N, persistent_token } (directly attributed)
Revocation/rejection
Portal: patient deleted / consent withdrawn / pseudonym rejected
Client → GET /practice/mappings?since=… → tombstone { revoked:true } ⇒ stop sending
Client → GET /rejected-clients → { rejected:[…] } ⇒ skip these
What your backend must provide (checklist)
So that the unmodified DATEYE client works against your backend:
- Tenant model: one “account” (practice) per setup code / API key; all data
account_id-scoped. - Setup codes: generation in the portal, storage as SHA-256 hash,
expires_at, statusactive/redeemed/invalidated,POST /setup/redeemincl. IP rate limit. - API keys: generation at redeem (
<prefix>_live_+ 64 hex), storage as SHA-256 hash + display prefix,revoked_at, bearer-auth middleware (SHA-256 lookup). -
encryptionKey: generate a per-account 32-byte key, store it encrypted at rest, serve it viaredeem/config; implementsetup/db-key(opaque blob, one-shot). - Pseudonym store:
devicePseudonym(32 hex,account_id-scoped) as the patient key; mappingpseudonym → clientIdwithmapped_at/ tombstone status. -
POST /import: the 4 modes,.strict()validation, batch 1–50, dedup on the natural key, counter response incl.skip_pseudonyms, consent check onfollow_up. - Pending queue + portal linking (manual),
persistentTokenissuance. -
GET /practice/mappings(incremental, tombstones, deterministic cursor),GET /rejected-clients,GET /imports/list. -
GET /health(auth),GET /status(public), error envelope + codes, 120/60s rate limit,Retry-After. - Privacy: never echo PII in error messages; strictly honor the pseudonym model (no PII over the API).
Versioning & Stability
All endpoints are prefixed with /api/v1/. Within v1, changes are additive and backward-compatible: new optional fields may appear in responses, new optional request fields may be accepted, and new error codes may be added. The client is expected to ignore unknown fields. Breaking changes (removed fields, changed semantics, incompatible request shapes) are only introduced under a new major version prefix (/api/v2), announced in advance. Deprecations are communicated via changelog and, where possible, via a response header before removal.
Security & Data Protection
- TLS only: all endpoints are HTTPS-only; plain HTTP is rejected.
- Privacy by design: the client never transmits cleartext PII — only pseudonyms (stable HMAC hashes), coarse hints (initials, birth month/year), and biometric measurements. The server cannot re-identify a patient from the data it receives. This design is aligned with GDPR data minimisation principles and is particularly important for health data under Art. 9 GDPR.
- API key storage: keys are stored exclusively as SHA-256 hashes; the plaintext is shown once at issuance and never retrievable again.
- Error messages: validation errors report only field paths and error codes — never the values received. PII is never echoed back in error responses.
The complete OpenAPI 3.0.3 definition is served at GET /api/v1/openapi.json and importable directly into Swagger, Postman, or any codegen tool.