> ## Documentation Index
> Fetch the complete documentation index at: https://api.idunox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Canonical submission JSON

> How Health Yourself intake data maps to canonical_submission_v1 for POST /v1/submissions (baseline platform contract).

Last updated: 2026-05-25 (aligned with platform baseline `488e91e` / tag `baseline`).

## Overview

Health Yourself collects questionnaire answers and blood results in your intake UX (see the [Input Data Preparation Guide](/api-reference/introduction)). Before calling Idunox, your backend **maps** that data into **canonical Type A JSON** — a structured `subject` block, a flat `markers[]` blood panel, optional `sourceMetadata`, and `options`.

<Warning>
  Do **not** send legacy `markers.healthQuestionnaireMarkers` / `markers.bloodMarkers` arrays or top-level `requestedAssessments`. Those shapes are not accepted by the live partner API.
</Warning>

| Concept                   | Canonical location                                                                         |
| ------------------------- | ------------------------------------------------------------------------------------------ |
| Questionnaire (36 items)  | `subject.demographics`, `subject.measurements`, `subject.history`, `subject.familyHistory` |
| Blood panel (14 analytes) | `markers[]` with `code`, `value`, `unit`                                                   |
| Lab reference intervals   | `sourceMetadata.markerRanges` (structured bounds per marker code)                          |
| Intake observation time   | `sourceMetadata.sourceTimestamp` (single timestamp for the bundle)                         |
| Wellbeing areas to score  | `options.requestedOutcomes` (public `wellbeing.*` ids)                                     |
| Report artefacts          | `options.requestedOutputs` (e.g. `json`, `html`, `pdf`, `inference_score_v1`)              |

## Top-level fields

| Field                 | Required    | Notes                                                                                                                       |
| --------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
| `schemaVersion`       | Yes         | Always `canonical_submission_v1`                                                                                            |
| `partnerId`           | Yes         | UUID of your **PartnerCredential** (must match the API key used in `Authorization`; mismatch → `403` `PARTNER_ID_MISMATCH`) |
| `partnerSubmissionId` | Yes         | Your unique submission reference (max 512 chars)                                                                            |
| `partnerSubjectId`    | Yes         | De-identified subject reference — no names, MRNs, email, or addresses                                                       |
| `subject`             | Yes         | `demographics` required; HY should also send `measurements`, `history`, and `familyHistory`                                 |
| `markers`             | Yes\*       | Non-empty array of **14** blood markers for Health Yourself                                                                 |
| `options`             | Yes         | `requestedOutputs` required (min 1); `requestedOutcomes` optional (max 8 entries)                                           |
| `sourceMetadata`      | Recommended | `sourceTimestamp`, `sourceSystem`, `labName`, `facilityId`, and `markerRanges`                                              |
| `questionnaire`       | Optional    | Future wellbeing Likert / B2B checklist only — **not** the 36 HY markers                                                    |
| `clinicalExtensions`  | Optional    | Reserved key bag (max 64 keys); still scanned for disallowed identifier field names                                         |
| `reportDocuments`     | Optional    | Alternative to `markers` when submitting report metadata only                                                               |

\* At least one of `markers` or `reportDocuments` must be non-empty. Health Yourself always sends the 14-marker panel in `markers`.

## Numeric sentinel (`-1`)

Optional integer and number fields on smoking and family history accept **`-1`** as “not applicable” (`TYPE_A_NOT_APPLICABLE` in the platform). The server treats `-1` like a missing value for branch rules (e.g. `stopAge` is not considered “set” when `-1`).

* Prefer **omitting** a key when the intake branch does not collect that field.
* Use **`-1`** only when your serializer must send a number but the UI path is “Not applicable”.
* Do not use `-1` on blood `markers[].value` — values must be finite numbers within range.

## Questionnaire → canonical mapping

Map intake labels to **canonical enum values** (lowercase snake\_case). Omit keys that do not apply; do not send `null` for optional numbers unless your serializer requires it.

### Demographics and measurements

| Intake (Q ID)          | Canonical path                                     | Canonical values                                                                               |
| ---------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Q001 Sex at birth      | `subject.demographics.sexAtBirth`                  | `male`, `female`                                                                               |
| Q002 Age               | `subject.demographics.ageYears` **or** `birthYear` | `ageYears`: integer `0..120`; **or** `birthYear`: `1900..2100` (at least one required)         |
| Q003 Weight            | `subject.measurements.weightKg`                    | number `0..300`                                                                                |
| Q004 Height            | `subject.measurements.heightCm`                    | number `0..250`                                                                                |
| Q005 Self-rated health | `subject.history.overallHealth`                    | `excellent`, `very_good`, `good`, `fair`, `poor`, `unknown` (map UI labels to lowercase enums) |

Optional demographics: `ethnicity` (string), `educationYears` (integer `0..40`).

### Smoking (`subject.history.smoking`)

**`status`** (required when `smoking` is sent): `never`, `former`, `current`, `prefer_not_to_say`

**`pastFrequency` / `currentFrequency`** (when allowed): `none`, `light`, `moderate`, `heavy`, `unknown`

| UKB / intake label            | Canonical frequency |
| ----------------------------- | ------------------- |
| `I have never smoked`         | `none`              |
| `Just tried once or twice`    | `light`             |
| `Only occasionally`           | `light`             |
| `On most or all days`         | `heavy`             |
| `No` (current-frequency auto) | `none`              |

| Intake                 | Canonical field     | Rules                                                                                                                                                      |
| ---------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Q006 Status            | `status`            | `Never smoker` → `never`; `Former smoker` → `former`; `Current smoker` → `current`                                                                         |
| Q007 Past frequency    | `pastFrequency`     | **Omit** when `status` is `never`, `current`, or `prefer_not_to_say`. For `former`, map labels per table above.                                            |
| Q008 Current frequency | `currentFrequency`  | **Only when `status: "current"`**. **Must be omitted** for `former`, `never`, and `prefer_not_to_say` (do not send `"none"` on former).                    |
| Q009 Current cigs/day  | `currentCigsPerDay` | number `0..200` or `-1`. **Only for `current`**; must be omitted for `former` / `never`.                                                                   |
| Q010 Past cigs/day     | `pastCigsPerDay`    | number `0..200` or `-1`. **Omit for `current` / `never`**.                                                                                                 |
| Q011 Stop age          | `stopAge`           | integer `0..120` or `-1`. **Required when `status: "former"`** (use `-1` on light/occasional former branches if needed). **Omit for `current` / `never`**. |
| Q012 Start age         | `startAge`          | integer `0..120` or `-1`. Heavy current or heavy former branch; otherwise omit.                                                                            |

**Current smokers:** provide **`currentCigsPerDay`** and/or **`currentFrequency`** (not `unknown`) so intensity is defined.

**Never / prefer not to say:** send only `status` (or omit the whole `smoking` object for never-only paths); all other smoking keys must be omitted.

<Accordion title="Former smoker example (canonical)">
  ```json theme={null}
  "smoking": {
    "status": "former",
    "pastFrequency": "heavy",
    "pastCigsPerDay": 12,
    "startAge": 19,
    "stopAge": 41
  }
  ```

  Do **not** include `currentFrequency` or `currentCigsPerDay`.
</Accordion>

### Conditions and medications

| Intake          | Canonical path                                       | Mapping                                                                                                                          |
| --------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Q013–Q017       | `subject.history.conditions.*`                       | `Yes` → `true`, `No` → `false` for `hasDiabetes`, `hasDementia`, `hasCardioCerebrovascular`, `hasLungCancer`, `hasKidneyDisease` |
| Q018 Medication | `subject.history.medications.takesRegularMedication` | `Yes` → `true`, `No` → `false`. If you send a `medications` object, **`takesRegularMedication` is required**.                    |

### Family history (`subject.familyHistory`)

Each parent (`father`, `mother`) uses `aliveStatus`, optional ages, and `conditions[]`.

| Intake                  | Canonical      | Mapping                                                                                                      |
| ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------------ |
| Q019 / Q028 Alive       | `aliveStatus`  | `Yes` → `alive`; `No` → `deceased`; `I do not know` → `unknown`                                              |
| Q020 / Q029 Death age   | `deathAge`     | integer `0..120` or `-1`. **Required when `aliveStatus: deceased`**. Omit or `-1` when `alive` or `unknown`. |
| Q021 / Q030 Current age | `currentAge`   | integer `0..120` or `-1`. **Required when `aliveStatus: alive`**. Omit or `-1` when `deceased` or `unknown`. |
| Q022–Q027 / Q031–Q036   | `conditions[]` | Each UI `Yes` adds a code; each `No` omits that code                                                         |

| UI “Yes” for…          | `conditions[]` code                   |
| ---------------------- | ------------------------------------- |
| Alzheimer's / Dementia | `alzheimer_disease` and/or `dementia` |
| Diabetes               | `diabetes_mellitus`                   |
| Heart disease          | `cardiovascular_disease`              |
| High blood pressure    | `hypertension`                        |
| Lung cancer            | `lung_cancer`                         |
| Stroke                 | `cerebrovascular_disease`             |

**All allowed `conditions[]` codes:** `diabetes_mellitus`, `cardiovascular_disease`, `cerebrovascular_disease`, `dementia`, `alzheimer_disease`, `kidney_disease`, `lung_cancer`, `other_malignancy`, `hypertension`, `none_known`, `unknown`.

<Warning>
  **Align `aliveStatus` with ages.** If father is `deceased`, send `deathAge` only (omit `currentAge`). If mother is `alive`, send `currentAge` only (omit `deathAge`). Swapping these causes validation errors.
</Warning>

## Blood markers → `markers[]`

Use canonical **marker codes** in `markers[].code` (not `B###` guide ids in the JSON body). Submit **exactly 14** rows — one per code, no duplicates. Aliases resolve to the same canonical code (e.g. `ALB` → `ALBUMIN`, `HBA1C` → `HBA1C_MMOL_MOL` with `mmol/mol` unit).

| B# (guide) | API `code`       | Required `unit` | Default analytical value range\* |
| ---------- | ---------------- | --------------- | -------------------------------- |
| B001       | `ALBUMIN`        | `g/L`           | `15..60`                         |
| B002       | `ALT`            | `U/L`           | `3..500`                         |
| B003       | `ALP`            | `U/L`           | `5..1500`                        |
| B006       | `AST`            | `U/L`           | `3..1000`                        |
| B007       | `CALCIUM`        | `mmol/L`        | `1..5`                           |
| B008       | `CHOL`           | `mmol/L`        | `0.5..18`                        |
| B010       | `CYSTATIN_C`     | `mg/L`          | `0.1..8.99`                      |
| B012       | `GGT`            | `U/L`           | `5..1200`                        |
| B014       | `HDL`            | `mmol/L`        | `0.05..4.65`                     |
| B015       | `CRP`            | `mg/L`          | `0.08..80`                       |
| B018       | `LDL`            | `mmol/L`        | `0.26..10.3`                     |
| B020       | `PHOSPHATE`      | `mmol/L`        | `0.32..6.4`                      |
| B028       | `URATE`          | `µmol/L`        | `89..1785`                       |
| B030       | `HBA1C_MMOL_MOL` | `mmol/mol`      | `15..515.2`                      |

\*When `sourceMetadata.markerRanges.{CODE}` is present for a marker, validation uses your `lowerBound..upperBound` instead of the preset analytical range. `µ` / `μ` in units normalize to the same value as `u` for comparison.

**Accepted code aliases (non-exhaustive):** `ALB`, `CA`, `TCHOL` / `TOTAL_CHOLESTEROL`, `CYSTATIN`, `HDL_CHOLESTEROL`, `C_REACTIVE_PROTEIN`, `LDL_DIRECT`, `PHOS`, `UA`, `HBA1C` (with `mmol/mol` unit only).

Each array element:

```json theme={null}
{ "code": "ALT", "value": 28, "unit": "U/L" }
```

* `value` must be a **finite number** (JSON number preferred).
* Optional per-marker `observedAt` (ISO-8601); otherwise use `sourceMetadata.sourceTimestamp` for the panel.

## Lab reference ranges (`sourceMetadata.markerRanges`)

Provide **structured** bounds keyed by marker code:

```json theme={null}
"markerRanges": {
  "ALT": { "unit": "U/L", "lowerBound": 7, "upperBound": 56 }
}
```

* `lowerBound` must be ≤ `upperBound`.
* `unit` must match the preset unit for that code (after normalization).
* When a code is listed, **value** validation uses your bounds instead of the default analytical preset.
* Unknown keys in `markerRanges` fail validation.
* Omit `markerRanges` entirely to use server preset analytical ranges only (still validates units, panel size, and duplicates).

## Options

```json theme={null}
"options": {
  "requestedOutputs": ["inference_score_v1", "json", "html"],
  "requestedOutcomes": [
    "wellbeing.cardiovascular",
    "wellbeing.cognitive",
    "wellbeing.renal",
    "wellbeing.respiratory"
  ]
}
```

| Field               | Description                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `requestedOutputs`  | Required array (min 1): `inference_score_v1`, `score`, `json`, `pdf`, `html`                                                                                                                                                                                                                                                                                                      |
| `requestedOutcomes` | Optional, max **8** entries. Public ids: `wellbeing.cardiovascular`, `wellbeing.renal`, `wellbeing.cognitive`, `wellbeing.respiratory`, `wellbeing.general`. Synonyms accepted: `wellbeing.kidney`, `renal.wellbeing` (renal). Legacy aliases (e.g. `cardiovascular_wellbeing_10y`) still normalize server-side. When omitted, the platform resolves defaults from configuration. |

Public outcome ids match `GET /v1/results` → `outcomes[].outcomeId`.

## Validation errors

| Stage                                  | HTTP | `error.code`                  | `details` shape                          |
| -------------------------------------- | ---- | ----------------------------- | ---------------------------------------- |
| Zod canonical body                     | 400  | `VALIDATION_ERROR`            | Zod flatten: `formErrors`, `fieldErrors` |
| `partnerId` ≠ API key credential       | 403  | `PARTNER_ID_MISMATCH`         | —                                        |
| 14-marker panel (codes, units, bounds) | 400  | `SUBMISSION_VALIDATION_ERROR` | Array of `{ field, message }`            |

See [Errors](/platform/errors) for examples.

## Further reading

* [Create Submissions](/api-reference/endpoint/submissions) — full request example
* [Input Data Preparation Guide](/api-reference/introduction) — intake UX tables (Q### / B###)
* [OpenAPI specification](/api-reference/endpoint/openapi) — machine-readable contract synced from the platform repo
