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

# How intake serving works

> A deeper look at the server-driven question loop: sessions, steps, presentation hints, and completion.

[How it works](/pages/integrate/how-it-works) introduces the three sockets your storefront connects
to. This page goes one level deeper on the first of them: how the intake questionnaire is actually
served, step by step, and exactly what your renderer is — and is not — responsible for.

## One loop, two calls

The whole protocol is a loop over two endpoints:

1. `GET /v1/instrument/resolve` starts a session — or resumes one — and returns the first step.
2. `POST /v1/instrument/next` submits the answer to the question the session is on and returns the
   next step.
3. Repeat until the response carries `status: complete`.

There is no "finish" call. Completion is a status the server declares once every required answer is
in — never something the client computes or asserts.

### The server is the sole authority

Your storefront can keep its own logic for polish — preloading a likely next screen, animating a
transition. Treat that logic as **advisory only**. The server alone decides which question comes
next, which questions are visible, and when the questionnaire is finished. The only way to advance
is to submit an answer and render what comes back. There is no client-side path around a required
question, no reordering, and no early exit into `complete` — whatever a browser extension, a cached
page, or a bug in your code does, the served flow holds.

## One session, two identifiers

* **`session_id`** is the handle for this run of the questionnaire. Every `next` and `abandon` call
  carries it.
* **`journey_id`** (prefixed `jny_`) is the patient's enrollment id, minted by the server on the
  first `resolve`. Send it back in the `X-Journey-Id` request header on a later `resolve` to resume:
  the patient gets a fresh session with everything they already answered applied, so nothing is
  asked twice. That header is how the hosted platform threads the patient's context — see
  [Identifiers and error types](/pages/integrate/identifiers-and-error-types#the-enrollment-thread).
  It is also the id you will see again on webhook events and the enrollment status read.

## Anatomy of a step

Every response in the loop — from `resolve`, `next`, and `abandon` alike — is the same shape: the
current step.

| Field                      | What it carries                                                                                                           |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `session_id`, `journey_id` | The session identity, as above                                                                                            |
| `status`                   | `active`, `complete`, or `abandoned`                                                                                      |
| `node`                     | The question or display screen to present now; `null` when the session is finished                                        |
| `flags`                    | Advisory markers, such as a detected mismatch between the validated shipping state and the state the patient asserted     |
| `issues`                   | Validation problems with the answer just submitted — present on a rejected answer                                         |
| `handoff`                  | The post-completion hand-off, present only once `status` is `complete` and only when the session began from an entry link |

## The rendered question

The `node` object is **presentation only** — everything about what the question *means* stays on
the server. Your renderer draws the screen; it never owns the sequence, the input type, or the
order of the choices.

| Field                        | What it carries                                                                                                                                                                                                                                                      |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node_id`                    | Stable identifier for this question or display screen                                                                                                                                                                                                                |
| `kind`                       | `question` (captures an answer) or `display` (an informational screen)                                                                                                                                                                                               |
| `section_id`                 | The section of the questionnaire this node belongs to                                                                                                                                                                                                                |
| `control`                    | Which input to draw — for example `single_select_cards`, `multi_select_cards`, `search_select`, `number`, `date`, `text`, or an address or file capture                                                                                                              |
| `option_codes`               | For choice controls: the stable codes of the selectable options, in served order                                                                                                                                                                                     |
| `options`                    | Optional per-option presentation hints — see below                                                                                                                                                                                                                   |
| `required`                   | Whether an answer is required to advance                                                                                                                                                                                                                             |
| `prefill`, `prefilled_value` | Prefill behavior for a value the platform already knows — see below                                                                                                                                                                                                  |
| `may_auto_advance`           | A hint that the renderer may advance immediately after a selection, without a separate continue button                                                                                                                                                               |
| `theme`                      | Your brand's presentation overlay — theme tokens for your renderer to apply. See [Brand configuration](/pages/integrate/brand-configuration)                                                                                                                         |
| `copy`                       | The question's headline text, served from the reviewed question wording in the compiled questionnaire — not brand copy. A brand overlay can restyle a question but can never supply or override this wording. Absent is valid; render from the code when it is unset |

A `display` node carries no answer; it may reference the offerings it presents through
`offering_refs`, and its values always come from the server — a display screen never asserts an
outcome the platform has not produced.

## Option codes and presentation hints

For choice controls, the answer you submit is always the **stable option code**, never display
text. Codes survive rewording, so a copy change on a choice never breaks a stored answer or your
integration.

When the step carries an `options` array, it lists the same codes as `option_codes`, in the same
order — served order is owned by the questionnaire and cannot be changed by theming — with
per-option hints:

* **`exclusive: true`** marks a "none of these"-class option that cannot be combined with any
  sibling. When the patient selects it, clear the other selections; when they select a sibling,
  clear the exclusive option. Resolve the conflict silently in the control — the latest selection
  wins — rather than showing an error message.
* **`classification_code`** rides on a banded choice — for example a blood-pressure or frequency
  range — so a patient-friendly band carries its classification alongside the stable code.

```json theme={null}
{
  "session_id": "sess_a1b2c3",
  "journey_id": "jny_8kQ2mX",
  "status": "active",
  "node": {
    "node_id": "conditions_current",
    "kind": "question",
    "section_id": "medical_history",
    "control": "multi_select_cards",
    "required": true,
    "option_codes": ["cond_diabetes_t2", "cond_hypertension", "cond_none"],
    "options": [
      { "code": "cond_diabetes_t2" },
      { "code": "cond_hypertension" },
      { "code": "cond_none", "exclusive": true }
    ]
  }
}
```

The `options` array is optional: when it is absent, render from `option_codes` alone. The
exclusive rule is enforced on the server either way — an answer that combines an exclusive code
with another selection is rejected with `422` and the session does not advance — so a renderer
without the hint still cannot submit a contradictory answer; it just learns about the conflict
from the response instead of preventing it in the control.

## One question per screen

Each step carries exactly one node — the wire itself enforces one question per screen. Use
`section_id` if you want to show which part of the questionnaire the patient is in; use
`may_auto_advance` to skip the continue button where the hint allows it.

Behind that screen the questionnaire holds two kinds of content apart, as described in
[How it works](/pages/integrate/how-it-works#two-kinds-of-question-one-hard-line): the marketing
and qualification screens you shape, and the medical questionnaire the physician side governs. The
separation is checked mechanically when the platform compiles a questionnaire — a question that
captures a medical fact cannot sit in a qualification section, and a questionnaire that tries
fails to compile. Your renderer never needs to tell them apart: every node arrives through the
same loop, already in its governed place.

## Prefill: ask once, confirm always

When the platform already holds a value for a question, the node's `prefill` field says what to do:

* **`suppress`** — the value is known and stable; the question is dropped from the flow entirely.
  A patient is never asked twice for something like a date of birth.
* **`confirm`** — the value is shown in `prefilled_value` for **explicit confirmation**. The
  confirm step always renders; a prefilled value is never accepted on the patient's behalf. This
  is also how a contact prefill from an entry link arrives — staged, unverified, and confirmed by
  the patient before anything is recorded.
* **`collect`** — nothing is known; ask normally.

## When an answer is rejected

Every answer is re-validated on the server, whatever the client already checked. A rejected answer
returns `422` with the same question re-presented and the specific problems in `issues`; the
session does not advance. Render the issues against the control and resubmit — the loop continues
exactly as before.

## Completion and the hand-off

Once every required answer is in, the response carries `status: complete` and a `null` node. If
the session began from an entry link with a validated entry context, the step also carries
`handoff`:

```json theme={null}
{
  "session_id": "sess_a1b2c3",
  "journey_id": "jny_8kQ2mX",
  "status": "complete",
  "node": null,
  "handoff": {
    "redirect": "https://storefront.example.com/thanks",
    "promo": "SPRING20",
    "test": true
  }
}
```

`redirect` is present only when it matched your brand's redirect allowlist — anything else was
dropped when the session started, so an unlisted URL can never surface here. `promo` is your
opaque token, passed through unchanged. The hand-off never carries patient data.

## Recording a drop-off

If the patient leaves without finishing, call `POST /v1/instrument/abandon` with the `session_id`.
The drop-off is recorded against the session, and the response is the same step shape with
`status: abandoned`. The way back in is a later `resolve` carrying the same `journey_id` in the
`X-Journey-Id` header — a fresh session that applies everything the patient already answered.
