One loop, two calls
The whole protocol is a loop over two endpoints:GET /v1/instrument/resolvestarts a session — or resumes one — and returns the first step.POST /v1/instrument/nextsubmits the answer to the question the session is on and returns the next step.- Repeat until the response carries
status: complete.
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 intocomplete — whatever a browser extension, a cached
page, or a bug in your code does, the served flow holds.
One session, two identifiers
session_idis the handle for this run of the questionnaire. Everynextandabandoncall carries it.journey_id(prefixedjny_) is the patient’s enrollment id, minted by the server on the firstresolve. Send it back in theX-Journey-Idrequest header on a laterresolveto 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 — fromresolve, next, and abandon alike — is the same shape: the
current step.
The rendered question
Thenode 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 anoptions 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: truemarks 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_coderides on a banded choice — for example a blood-pressure or frequency range — so a patient-friendly band carries its classification alongside the stable code.
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. Usesection_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’sprefill 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 inprefilled_valuefor 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 returns422 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 carriesstatus: 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, callPOST /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.