Skip to main content
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. 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.

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. 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.
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: 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:
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.