Stateful mock of the Optimal DX (ODX) partner API for test suites: partner labs and their
biomarker elements, practice patients (create, update, delete, partner link, search), lab
imports (HL7 v2 ORU and structured results), the Functional Health Report (JSON or PDF),
webhook registrations, and the signed PatientTest webhooks ODX posts back.
The vendor was retired 2026-07-22. Our queue paths are gated only by the
geviti-pdf-enabledflag (defaultfalse), so a local stack with no PostHog still calls ODX. The cheaper fix is to turn that flag on through the PostHog mock (@crvouga/mockingbird-service-posthog); use this mock when a suite must exercise the ODX path itself (bio-age webhooks, the Healthie PDF upload, migrations).
- Operation coverage: SUPPORT.md
- ODX publishes no spec:
openapi.yamlis hand-authored from our consumer's wire shapes (optimal.dx.service.ts, the webhook guard and DTO, and QA'sodx-client.ts).
Install
npm install -D @crvouga/mockingbird-service-odxESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
npx mockingbird-odx serve, createServer from ./server (Node), or createRuntime with any
Fetch server.
Usage
Point OPTIMAL_URL at the mock (it is overridable; no path prefix is needed), keep any
OPTIMAL_API_KEY and OPTIMAL_PRACTICE_ID. Pre-register the backend's webhook, or let
manageWebhooks register it through POST /v1/webhook as it does in production:
npx mockingbird-odx serve --port 8817 \
--webhook-url http://127.0.0.1:3000/odx/webhook # must equal SYSTEM_API_DEPLOYMENT_URL/odx/webhookimport { createRuntime } from "@crvouga/mockingbird-service-odx"
const odx = createRuntime({ webhook: { url: "http://127.0.0.1:3000/odx/webhook" } })
const call = (path: string, body?: unknown) =>
odx.fetch(
new Request(`http://odx.test${path}`, {
method: body === undefined ? "GET" : "POST",
headers: { ApiKey: "any", "content-type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
}),
)
const patient = await (
await call("/v1/practice/p1/patient", { firstName: "Ada", lastName: "Lovelace", gender: "Female", email: "ada@example.com" })
).json()
// β¦the app posts HL7 to /v1/practice/p1/patient/{patientId}/test; the mock answers the parsed
// PatientTest and posts a signed `Created` webhook to /odx/webhook.Routes
| Route | Behaviour |
|---|---|
GET /v1/partner/labs |
[{labId, name, isCurrentLab}]: AHA (1), Quest (2), LabCorp (3). |
GET /v1/elements/{labId} |
The element corpus (ELEMENTS) for the lab: elementId, elementName, elementGenderType, cuUnit, siUnit, cuToSiConversionFactor, elementReferences[{elementCode}] (LOINC codes HL7 OBX-3 is mapped by). Includes the nine phenotypic-age inputs (ids 494, 496, 506, 511, 537/538, 556, 564, 568, 571). Unknown lab β 404 {Message}. |
POST /v1/practice/{pid}/patient |
Creates an OdxPatient with a numeric patientId (100001β¦). firstName, lastName, email required, else ASP.NET problem details 400. dateOfBirth becomes YYYY-MM-DDT00:00:00; gender is normalized to Male/Female/Unknown. |
PUT /v1/practice/{pid}/patient/{id} |
Replaces the patient; 404 {Message} when unknown in that practice. |
DELETE /v1/practice/{pid}/patient/{id} |
204; drops its tests (QA teardown). |
POST /v1/practice/{pid}/patient/{id}/partner/{localUserId} |
Body is the literal false; stores our user id (admin-visible). Answers true. |
GET /v1/practice/{pid}/patients |
All patients; with any of email, firstName, lastName, dateOfBirth it is a case-insensitive search that answers 404 when nothing matches (our client passes ignore404). |
POST β¦/patient/{id}/test |
HL7 import: {labProfileId, labId, testDate, unitType, userId, externalReference, externalMessageControlId?, externalPatientTestId, menstrualPhase, isFasting, hl7}. OBX-3 code^text maps to an element by lab code, EL<id>, or name (hs-CRP picks the patient's sex); OBX-5 values like <0.2 become comparison: "<". Unmapped / non-numeric observations land in importLogs. Emits Created. A message without MSH or OBX is a 400. |
PUT β¦/patient/{id}/test/{testId} |
Re-import; emits Updated (ODX sends a Created+Updated pair with identical data). |
POST β¦/patient/{id}/testresults |
Structured import (results[{elementId, value, comparison}]); emits Created. |
GET β¦/patient/{id}/tests |
The patient's tests, oldest first. |
POST /v1/reports/FunctionalHealthReport |
outputType: Json β {metadata, labs, elements, sections} with the sections our storeHealthData reads (groups, above/below optimal, "Functional Body Systems" conditions, health concerns); Pdf β a valid single-page application/pdf. Unknown test β 404. |
GET /v1/webhooks |
[{partnerWebhookId, signingKey, createDate, entityEvents: {PatientTest}, webhookUrl}] β our guard calls this on every inbound webhook to fetch the key. |
POST /v1/webhook, PUT /v1/webhook/{id} |
Register / update (manageWebhooks). |
Errors: missing / unknown ApiKey β 401 {statusCode, message} (Azure API Management's shape);
ODX errors β {Message}; validation β ASP.NET problem details {title, status, errors}.
Webhooks
Every test import or re-import posts {entityType: "PatientTest", eventType, data: <PatientTest>}
to each registered webhook URL subscribed to that event, with
optimaldx-signature: <UPPERCASE hex HMAC-SHA256(signingKey, rawBody)>. data is the full
PatientTest (results[].comparison is always a string, as our receiver's zod DTO requires).
Non-2xx answers are retried (immediately, 5 s, 5 min, 30 min, 2 h). GET /__admin/webhooks,
/__admin/webhooks/events, POST /__admin/webhooks/flush and β¦/replay work as usual.
Admin (beyond the standard contract)
| Route | Effect |
|---|---|
POST /__admin/tests/:id/webhook |
{eventType?: Created|Updated|Deleted, signature?: valid|short|bad}: emit a webhook for a stored test. |
DELETE /__admin/tests/:id |
Emit Deleted and drop the test. |
GET /__admin/patients, GET /__admin/tests |
The namespace's records (patients include partnerUserId; the HL7 text is never stored). |
GET/PUT /__admin/settings |
{apiKeys?: string[]} (empty accepts any key). |
Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}): wrong_length_signature,
bad_signature, empty_success, no_content, not_found, server_error, slow,
webhook_duplicate, webhook_reorder, webhook_drop.
Namespaces
x-mockingbird-namespace, a /ns/<name> prefix on OPTIMAL_URL, or by API key:
PUT /__admin/credentials {"credentials": {"<OPTIMAL_API_KEY>": "<namespace>"}}.
Known consumer bugs
- Wrong-length signature β 500.
OdxSignatureGuard.verifyHmaccallscrypto.timingSafeEqualon buffers of different lengths, which throwsRangeErrorinstead of returning false, so a truncatedoptimaldx-signatureis a 500 (ODX then retries) rather than a- The
wrong_length_signaturepreset sends a 32-character signature to reproduce it.
- The
manageWebhooks"updates" a stale registration with its own old URL (a no-op), then registers the new one, so stale registrations accumulate.updateWebhookpasses the method as'Put'; Node's fetch normalizes it (Bun's does not β the consumer port normalizes, as the app runs on Node).
Deliberately not modelled
- Clinical logic: report conditions and health concerns are derived only from which results fall outside their optimal range; bio-age is not computed.
- The real element catalog (thousands of elements, lab-specific codes): a 13-element corpus covers our consumer's needs. HL7 segments other than MSH/OBX are ignored; PID is never stored.
- Report themes, recipients beyond the metadata label, and PDF content.
- No official SDK exists, so there is no SDK drop-in test.
API
| Export | Kind | Description |
|---|---|---|
OdxAPI |
class | The in-process mock: fetch(request), reset(), emit(testId, eventType, signature?), patients(), tests(). Options: sqlite, now, namespace, settings, onWebhook. |
createRuntime |
function | The mock with the full service contract. Options: webhook: {url, signingKey?}, settings, retryDelaysMs, fetch, clock, seed, adminKey, onLog. |
ODX_PRESETS |
object | Every named fault preset. |
ODX_NAMESPACE |
string | The service name, "odx". |
SIGNATURE_HEADER |
string | "optimaldx-signature". |
signOdx |
function | (signingKey, body) β UPPERCASE hex HMAC-SHA256. |
apiKeyCredential |
function | The ApiKey header (how credentials map to namespaces). |
ELEMENTS, LABS |
arrays | The element corpus and partner labs. |
matchElement, parseObservations |
functions | HL7 OBX parsing and code β element mapping. |
DEFAULT_SETTINGS |
object | Per-namespace defaults. |
document, operationIds, supportedOperationIds |
values | The vendored OpenAPI contract and its operation ids. |
createServer, serveTarget, DEFAULT_PORT (./server) |
Node | Serve over node:http; the serve CLI target; port 8817. |
Part of mockingbird.