Onboarding & Authentication
Redeem setup code — POST /api/v1/setup/redeem
Unauthenticated (IP rate limit). Exchanges a one-time setup code for credentials.
Setup-code format: ^(MK|MD)-[A-Z2-9]{4}-[A-Z2-9]{4}$
Alphabet 23456789ABCDEFGHJKMNPQRSTUVWXYZ (31 chars, excludes 0/1/I/L/O — low ambiguity).
The prefix is free to choose (MK = prod, MD = dev in the reference implementation). The server stores only the SHA-256 hash of the code (code_hash), never the plaintext.
Request:
{ "code": "MK-7X3F-9K2P", "hostname": "PRACTICE-PC-01" }
hostname is optional (printable ASCII, ≤255).
Response 200:
{
"success": true,
"data": {
"apiKey": "mk_live_1a2b…(64 hex)",
"host": "https://myopia.kids",
"encryptionKey": "e3f1…(64 hex)"
}
}
apiKeyis returned in plaintext only here, once (never retrievable again).encryptionKey= per-account key used only for the DB-passphrase backup (see below), not for import data.
Errors:
| Code | HTTP | Meaning |
|---|---|---|
INVALID_CODE | 400 | Bad format or unknown code |
INVALID_HOSTNAME | 400 | hostname not printable ASCII / >255 |
ALREADY_REDEEMED | 409 | Code already redeemed |
CODE_EXPIRED | 410 | Code expired (expires_at < now) |
RATE_LIMIT_EXCEEDED | 429 | >10 attempts/h/IP (with Retry-After) |
INVALID_REQUEST | 400 | JSON not parseable |
INTERNAL_ERROR | 500 | Server error |
API key & authentication
- Format:
mk_live_+ 64 hex chars (32 random bytes). The prefix is cosmetic — pick your own for your product (e.g.mc_live_). - Server-side storage: Only the SHA-256 hash (
key_hash, hex) plus a truncated display prefix (key_prefix). The plaintext key is never stored. - Validating an incoming request: Extract the bearer token → compute
SHA-256→SELECT … FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL. No match →401 INVALID_API_KEY. A match yields theaccount_id(tenant) for the rest of the request. - Side effect: On every authenticated request, update
last_used_at(and the telemetry headers) fire-and-forget — it must not block the response.
Fetch encryption key — GET /api/v1/config
Authenticated. Returns the same encryptionKey again (e.g. after a reinstall) plus the active update channel.
Response 200:
{ "success": true, "data": { "encryptionKey": "e3f1…", "updateChannel": "stable" } }
updateChannel = stable | canary (from the key’s canary_consent flag; the header X-DATEYE-Canary-Consent overrides it for the current response).
DB-passphrase backup (using the encryptionKey)
The DATEYE client encrypts local patient data in its own DB with a passphrase. For recovery after device loss, it can deposit this passphrase with the server client-side encrypted. The server stores only an opaque blob and cannot decrypt it.
Encryption format (AES-256-GCM):
iv_hex : authTag_hex : ciphertext_hex
- Algorithm
aes-256-gcm, key = the 32 bytes fromencryptionKey(hex-decoded) iv= 16 random bytes,authTag= 16 bytes (GCM), all parts hex, separated by:- This is exactly the format the client produces/expects itself.
POST /api/v1/setup/db-key — store blob (one-shot)
{ "encryptedDbKey": "iv_hex:authTag_hex:ciphertext_hex" }
Stores only if no blob exists yet (… WHERE dateye_db_key IS NULL). A second call → 409 KEY_ALREADY_SET (signal to the client: GET first and compare locally). Response { "success": true }.
GET /api/v1/setup/db-key — retrieve blob
{ "success": true, "data": { "encryptedDbKey": "iv_hex:authTag_hex:ciphertext_hex" } }
No blob stored → 404 KEY_NOT_FOUND. The client decrypts locally with encryptionKey.
There is historically a second path
/api/v1/config/db-keywhere the server en/decrypts and stores overwritably. For the OEM integration the/setup/db-keypath (opaque blob, no server decrypt) is the authoritative, privacy-conforming one./config/db-keycan be omitted.