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
question node’s answer is shaped by its control: see
controls and their answers for every control. Always send the served
node_id with the answer.
A display node carries no answer — send its node_id with no answer to move past it. 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.
Controls and their answers
Every question node names onecontrol. These are all the values the server serves, and the
answer each one takes in the answer object of POST /v1/instrument/next:
How the answers work:
- Choice controls (
single_select_cards,multi_select_cards,search_select) send stable option codes fromoption_codes, never display text. Whether one code or several may be sent comes from the question, not the control: a question that takes one answer refuses a second code, even onsearch_select. See option codes for the exclusive rule. - Number controls (
scale,number) send a JSON number. A number sent as a string is refused. When the node carries aunit, show it beside the field. number_pairsendspair, an array of exactly two numbers.- Text-shaped controls (
date,text,long_text,phone,email) send a string.date,phoneandemailare checked for their shape: a date must beYYYY-MM-DDand a real day, a phone a ten-digit US number, and a value such asN/Ais refused with an entry inissues. An answer of only spaces on a required question counts as no answer. addresssends either thesuggestion_idof a suggestion fromGET /v1/instrument/address/suggestor amanualaddress withline1,city,state,postal_codeandcountry(and an optionalline2). When the address’s state differs from the state the patient gave earlier, the step carries a flag. To go ahead despite the mismatch, include"override_state_mismatch": truein theaddressanswer.file_capture(a file upload question: photo, video or document) is never answered on your site. Your storefront’s intake does not ask the patient for it: the patient answers it in the member portal after they sign in, so their files never pass through your site or your server. When the questionnaire reaches such a question, the step marks it as continuing in the member portal and gives you the link to send the patient there. Send the patient to that link; there is no answer for your storefront to submit.
- A prefilled value you show for confirmation (
prefill: confirm) is confirmed with{"confirmed": true}. See prefill. - A
consentnode is answered with{"consent": {"acknowledged": true}}to accept or{"consent": {"acknowledged": false}}to decline, with the node’sconsent_versionasversion. Declining a treatment consent ends the questionnaire withstatus: blocked. A node that carriesconsent_kind: communications_consent(an email or text message opt-in) never blocks: accept and decline both move on.
control as a
fault to fix in your renderer.
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.