curl --request POST \
--url https://api.dev.purplelabelmd.com/v1/instrument/next \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'X-Brand-Id: <x-brand-id>' \
--data '
{
"session_id": "<string>",
"answer": {
"address": {
"override_state_mismatch": true,
"suggestion_id": "<string>"
},
"codes": [
"<string>"
],
"confirmed": true,
"media": {
"byte_size": 123,
"content_type": "<string>",
"duration_s": 123,
"upload_ref": "<string>"
},
"pair": [
123
],
"value": "<unknown>"
},
"node_id": "<string>"
}
'import requests
url = "https://api.dev.purplelabelmd.com/v1/instrument/next"
payload = {
"session_id": "<string>",
"answer": {
"address": {
"override_state_mismatch": True,
"suggestion_id": "<string>"
},
"codes": ["<string>"],
"confirmed": True,
"media": {
"byte_size": 123,
"content_type": "<string>",
"duration_s": 123,
"upload_ref": "<string>"
},
"pair": [123],
"value": "<unknown>"
},
"node_id": "<string>"
}
headers = {
"X-Brand-Id": "<x-brand-id>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Brand-Id': '<x-brand-id>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: '<string>',
answer: {
address: {override_state_mismatch: true, suggestion_id: '<string>'},
codes: ['<string>'],
confirmed: true,
media: {
byte_size: 123,
content_type: '<string>',
duration_s: 123,
upload_ref: '<string>'
},
pair: [123],
value: '<unknown>'
},
node_id: '<string>'
})
};
fetch('https://api.dev.purplelabelmd.com/v1/instrument/next', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.dev.purplelabelmd.com/v1/instrument/next",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'session_id' => '<string>',
'answer' => [
'address' => [
'override_state_mismatch' => true,
'suggestion_id' => '<string>'
],
'codes' => [
'<string>'
],
'confirmed' => true,
'media' => [
'byte_size' => 123,
'content_type' => '<string>',
'duration_s' => 123,
'upload_ref' => '<string>'
],
'pair' => [
123
],
'value' => '<unknown>'
],
'node_id' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"X-Brand-Id: <x-brand-id>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.dev.purplelabelmd.com/v1/instrument/next"
payload := strings.NewReader("{\n \"session_id\": \"<string>\",\n \"answer\": {\n \"address\": {\n \"override_state_mismatch\": true,\n \"suggestion_id\": \"<string>\"\n },\n \"codes\": [\n \"<string>\"\n ],\n \"confirmed\": true,\n \"media\": {\n \"byte_size\": 123,\n \"content_type\": \"<string>\",\n \"duration_s\": 123,\n \"upload_ref\": \"<string>\"\n },\n \"pair\": [\n 123\n ],\n \"value\": \"<unknown>\"\n },\n \"node_id\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Brand-Id", "<x-brand-id>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.dev.purplelabelmd.com/v1/instrument/next")
.header("X-Brand-Id", "<x-brand-id>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"session_id\": \"<string>\",\n \"answer\": {\n \"address\": {\n \"override_state_mismatch\": true,\n \"suggestion_id\": \"<string>\"\n },\n \"codes\": [\n \"<string>\"\n ],\n \"confirmed\": true,\n \"media\": {\n \"byte_size\": 123,\n \"content_type\": \"<string>\",\n \"duration_s\": 123,\n \"upload_ref\": \"<string>\"\n },\n \"pair\": [\n 123\n ],\n \"value\": \"<unknown>\"\n },\n \"node_id\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.dev.purplelabelmd.com/v1/instrument/next")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Brand-Id"] = '<x-brand-id>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"session_id\": \"<string>\",\n \"answer\": {\n \"address\": {\n \"override_state_mismatch\": true,\n \"suggestion_id\": \"<string>\"\n },\n \"codes\": [\n \"<string>\"\n ],\n \"confirmed\": true,\n \"media\": {\n \"byte_size\": 123,\n \"content_type\": \"<string>\",\n \"duration_s\": 123,\n \"upload_ref\": \"<string>\"\n },\n \"pair\": [\n 123\n ],\n \"value\": \"<unknown>\"\n },\n \"node_id\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"journey_id": "<string>",
"session_id": "<string>",
"status": "active",
"blocked": {
"code": "<string>",
"node_id": "<string>",
"support_route": "<string>",
"ungranted_scopes": [
"<string>"
]
},
"flags": [
"<string>"
],
"handoff": {
"promo": "<string>",
"redirect": "<string>",
"test": true
},
"issues": [
{
"code": "<string>",
"message": "<string>",
"conflicting_codes": [
"<string>"
],
"exclusive_codes": [
"<string>"
]
}
],
"node": {
"kind": "question",
"node_id": "<string>",
"section_id": "<string>",
"computed_stub": true,
"consent_version": "<string>",
"content": {},
"control": "<string>",
"copy": "<string>",
"display": "static",
"fact": "<string>",
"grants": [
"<string>"
],
"interstitial": true,
"may_auto_advance": true,
"media": {
"kind": "image",
"accept": [
"<string>"
],
"capture_mode": "upload",
"consent_copy": "<string>",
"consent_fact": "<string>",
"consent_version": "<string>",
"facing": "user",
"max_bytes": 123,
"max_duration_s": 123,
"min_duration_s": 123,
"require_consent": true,
"resumable": true
},
"offering_refs": [
"<string>"
],
"option_codes": [
"<string>"
],
"options": [
{
"code": "<string>",
"classification_code": "<string>",
"exclusive": true,
"label": "<string>"
}
],
"prefill": "collect",
"prefilled_value": "<unknown>",
"reassurance": "<string>",
"required": true,
"theme": {},
"values": {},
"zone": {
"scale_max": 123,
"scale_min": 123,
"zone_label": "<string>",
"zone_max": 123,
"zone_min": 123
}
},
"progress": {
"position_estimate": 0.5,
"scope": "<string>",
"section_label": "<string>"
}
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}{
"journey_id": "<string>",
"session_id": "<string>",
"status": "active",
"blocked": {
"code": "<string>",
"node_id": "<string>",
"support_route": "<string>",
"ungranted_scopes": [
"<string>"
]
},
"flags": [
"<string>"
],
"handoff": {
"promo": "<string>",
"redirect": "<string>",
"test": true
},
"issues": [
{
"code": "<string>",
"message": "<string>",
"conflicting_codes": [
"<string>"
],
"exclusive_codes": [
"<string>"
]
}
],
"node": {
"kind": "question",
"node_id": "<string>",
"section_id": "<string>",
"computed_stub": true,
"consent_version": "<string>",
"content": {},
"control": "<string>",
"copy": "<string>",
"display": "static",
"fact": "<string>",
"grants": [
"<string>"
],
"interstitial": true,
"may_auto_advance": true,
"media": {
"kind": "image",
"accept": [
"<string>"
],
"capture_mode": "upload",
"consent_copy": "<string>",
"consent_fact": "<string>",
"consent_version": "<string>",
"facing": "user",
"max_bytes": 123,
"max_duration_s": 123,
"min_duration_s": 123,
"require_consent": true,
"resumable": true
},
"offering_refs": [
"<string>"
],
"option_codes": [
"<string>"
],
"options": [
{
"code": "<string>",
"classification_code": "<string>",
"exclusive": true,
"label": "<string>"
}
],
"prefill": "collect",
"prefilled_value": "<unknown>",
"reassurance": "<string>",
"required": true,
"theme": {},
"values": {},
"zone": {
"scale_max": 123,
"scale_min": 123,
"zone_label": "<string>",
"zone_max": 123,
"zone_min": 123
}
},
"progress": {
"position_estimate": 0.5,
"scope": "<string>",
"section_label": "<string>"
}
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}Submit an answer and get the next question
Submits the answer to the question the session is currently on. The answer is re-validated on the server and the session advances exactly one step, returning the next question — or status: complete once every required answer is in. The client never decides completion. If the answer fails validation the response is 422 with the specific issues and the session does not advance. Send node_id (the served node.node_id) to KEY the answer to its node: a stale replay of an already-passed node then resyncs cleanly (200, current node re-served) instead of misbinding to the node after a server-side skip, and a submit for a node the session has not reached fails closed with a renderer.node_mismatch issue (422). This op never presents a question that collects a government ID, and never waits on one to report complete — proof of identity is handled in the member portal’s identity step, not here.
curl --request POST \
--url https://api.dev.purplelabelmd.com/v1/instrument/next \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'X-Brand-Id: <x-brand-id>' \
--data '
{
"session_id": "<string>",
"answer": {
"address": {
"override_state_mismatch": true,
"suggestion_id": "<string>"
},
"codes": [
"<string>"
],
"confirmed": true,
"media": {
"byte_size": 123,
"content_type": "<string>",
"duration_s": 123,
"upload_ref": "<string>"
},
"pair": [
123
],
"value": "<unknown>"
},
"node_id": "<string>"
}
'import requests
url = "https://api.dev.purplelabelmd.com/v1/instrument/next"
payload = {
"session_id": "<string>",
"answer": {
"address": {
"override_state_mismatch": True,
"suggestion_id": "<string>"
},
"codes": ["<string>"],
"confirmed": True,
"media": {
"byte_size": 123,
"content_type": "<string>",
"duration_s": 123,
"upload_ref": "<string>"
},
"pair": [123],
"value": "<unknown>"
},
"node_id": "<string>"
}
headers = {
"X-Brand-Id": "<x-brand-id>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'X-Brand-Id': '<x-brand-id>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: '<string>',
answer: {
address: {override_state_mismatch: true, suggestion_id: '<string>'},
codes: ['<string>'],
confirmed: true,
media: {
byte_size: 123,
content_type: '<string>',
duration_s: 123,
upload_ref: '<string>'
},
pair: [123],
value: '<unknown>'
},
node_id: '<string>'
})
};
fetch('https://api.dev.purplelabelmd.com/v1/instrument/next', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.dev.purplelabelmd.com/v1/instrument/next",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'session_id' => '<string>',
'answer' => [
'address' => [
'override_state_mismatch' => true,
'suggestion_id' => '<string>'
],
'codes' => [
'<string>'
],
'confirmed' => true,
'media' => [
'byte_size' => 123,
'content_type' => '<string>',
'duration_s' => 123,
'upload_ref' => '<string>'
],
'pair' => [
123
],
'value' => '<unknown>'
],
'node_id' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"X-Brand-Id: <x-brand-id>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.dev.purplelabelmd.com/v1/instrument/next"
payload := strings.NewReader("{\n \"session_id\": \"<string>\",\n \"answer\": {\n \"address\": {\n \"override_state_mismatch\": true,\n \"suggestion_id\": \"<string>\"\n },\n \"codes\": [\n \"<string>\"\n ],\n \"confirmed\": true,\n \"media\": {\n \"byte_size\": 123,\n \"content_type\": \"<string>\",\n \"duration_s\": 123,\n \"upload_ref\": \"<string>\"\n },\n \"pair\": [\n 123\n ],\n \"value\": \"<unknown>\"\n },\n \"node_id\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Brand-Id", "<x-brand-id>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.dev.purplelabelmd.com/v1/instrument/next")
.header("X-Brand-Id", "<x-brand-id>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"session_id\": \"<string>\",\n \"answer\": {\n \"address\": {\n \"override_state_mismatch\": true,\n \"suggestion_id\": \"<string>\"\n },\n \"codes\": [\n \"<string>\"\n ],\n \"confirmed\": true,\n \"media\": {\n \"byte_size\": 123,\n \"content_type\": \"<string>\",\n \"duration_s\": 123,\n \"upload_ref\": \"<string>\"\n },\n \"pair\": [\n 123\n ],\n \"value\": \"<unknown>\"\n },\n \"node_id\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.dev.purplelabelmd.com/v1/instrument/next")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Brand-Id"] = '<x-brand-id>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"session_id\": \"<string>\",\n \"answer\": {\n \"address\": {\n \"override_state_mismatch\": true,\n \"suggestion_id\": \"<string>\"\n },\n \"codes\": [\n \"<string>\"\n ],\n \"confirmed\": true,\n \"media\": {\n \"byte_size\": 123,\n \"content_type\": \"<string>\",\n \"duration_s\": 123,\n \"upload_ref\": \"<string>\"\n },\n \"pair\": [\n 123\n ],\n \"value\": \"<unknown>\"\n },\n \"node_id\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"journey_id": "<string>",
"session_id": "<string>",
"status": "active",
"blocked": {
"code": "<string>",
"node_id": "<string>",
"support_route": "<string>",
"ungranted_scopes": [
"<string>"
]
},
"flags": [
"<string>"
],
"handoff": {
"promo": "<string>",
"redirect": "<string>",
"test": true
},
"issues": [
{
"code": "<string>",
"message": "<string>",
"conflicting_codes": [
"<string>"
],
"exclusive_codes": [
"<string>"
]
}
],
"node": {
"kind": "question",
"node_id": "<string>",
"section_id": "<string>",
"computed_stub": true,
"consent_version": "<string>",
"content": {},
"control": "<string>",
"copy": "<string>",
"display": "static",
"fact": "<string>",
"grants": [
"<string>"
],
"interstitial": true,
"may_auto_advance": true,
"media": {
"kind": "image",
"accept": [
"<string>"
],
"capture_mode": "upload",
"consent_copy": "<string>",
"consent_fact": "<string>",
"consent_version": "<string>",
"facing": "user",
"max_bytes": 123,
"max_duration_s": 123,
"min_duration_s": 123,
"require_consent": true,
"resumable": true
},
"offering_refs": [
"<string>"
],
"option_codes": [
"<string>"
],
"options": [
{
"code": "<string>",
"classification_code": "<string>",
"exclusive": true,
"label": "<string>"
}
],
"prefill": "collect",
"prefilled_value": "<unknown>",
"reassurance": "<string>",
"required": true,
"theme": {},
"values": {},
"zone": {
"scale_max": 123,
"scale_min": 123,
"zone_label": "<string>",
"zone_max": 123,
"zone_min": 123
}
},
"progress": {
"position_estimate": 0.5,
"scope": "<string>",
"section_label": "<string>"
}
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}{
"journey_id": "<string>",
"session_id": "<string>",
"status": "active",
"blocked": {
"code": "<string>",
"node_id": "<string>",
"support_route": "<string>",
"ungranted_scopes": [
"<string>"
]
},
"flags": [
"<string>"
],
"handoff": {
"promo": "<string>",
"redirect": "<string>",
"test": true
},
"issues": [
{
"code": "<string>",
"message": "<string>",
"conflicting_codes": [
"<string>"
],
"exclusive_codes": [
"<string>"
]
}
],
"node": {
"kind": "question",
"node_id": "<string>",
"section_id": "<string>",
"computed_stub": true,
"consent_version": "<string>",
"content": {},
"control": "<string>",
"copy": "<string>",
"display": "static",
"fact": "<string>",
"grants": [
"<string>"
],
"interstitial": true,
"may_auto_advance": true,
"media": {
"kind": "image",
"accept": [
"<string>"
],
"capture_mode": "upload",
"consent_copy": "<string>",
"consent_fact": "<string>",
"consent_version": "<string>",
"facing": "user",
"max_bytes": 123,
"max_duration_s": 123,
"min_duration_s": 123,
"require_consent": true,
"resumable": true
},
"offering_refs": [
"<string>"
],
"option_codes": [
"<string>"
],
"options": [
{
"code": "<string>",
"classification_code": "<string>",
"exclusive": true,
"label": "<string>"
}
],
"prefill": "collect",
"prefilled_value": "<unknown>",
"reassurance": "<string>",
"required": true,
"theme": {},
"values": {},
"zone": {
"scale_max": 123,
"scale_min": 123,
"zone_label": "<string>",
"zone_max": 123,
"zone_min": 123
}
},
"progress": {
"position_estimate": 0.5,
"scope": "<string>",
"section_label": "<string>"
}
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}{
"status": 123,
"title": "<string>",
"type": "<string>",
"detail": "<string>"
}Authorizations
Per-client API key (M2M). Presented as Authorization: Bearer <key>.
Headers
An optional id you send to tie this request to your own logs. Send one and it is echoed back unchanged; omit it and one is assigned for you. Either way the id is returned in the X-Correlation-Id response header on every response, including errors, so you can match a response to the request that produced it.
128^[A-Za-z0-9][A-Za-z0-9._:-]*$Opaque brand id (brd_...). Validated to belong to the resolved client (§2).
^brd_[A-Za-z0-9][A-Za-z0-9_-]*$Body
An answer for the question the session is currently on. Send node_id (recommended) to KEY the answer to the node it was collected for so a server-side skip or a restart can never land it on the wrong node.
A patient's answer for the current question, interpreted per its control type. Coded controls carry codes; scalar controls carry value; a number pair carries pair; a pre-filled-value confirmation carries confirmed; the address control carries address; a video capture carries media (an opaque upload reference + descriptor) and consent.
Show child attributes
Show child attributes
The id of the node this answer was collected for (echo the served node.node_id). ADDITIVE and OPTIONAL. When present the server binds the answer to that node instead of blindly to the current cursor: if it matches the current node the submit proceeds; if it names a node ALREADY PASSED (a stale replay after a serve-time skip or a restart) the server re-serves the current node unchanged (idempotent resync — no misbind, no write); if it names a node the session has not reached (or an unknown node) the submit fails closed with a renderer.node_mismatch issue (422), the current node re-presented. Absent ⇒ the legacy un-keyed binding (byte-identical) — which is why an un-keyed client can desync after a skip.
Response
the next node, or completion
The result of a resolve, next, or abandon call — the session identity, the current question (or null when finished), the status, any flags (e.g. ship_state_mismatch), and any validation issues. On completion it may carry the entry-link hand-off.
^jny_[A-Za-z0-9][A-Za-z0-9_-]*$active (a node is presented), complete (server-authoritative completion), abandoned (patient drop-off), or blocked — a terminal, server-authoritative refusal to proceed (a required consent was declined or could not be affirmed). On blocked, node is null and blocked carries the honest stop-screen outcome.
active, complete, abandoned, blocked The honest stop-screen outcome. Present only when status is blocked. Carries the machine-readable reason, the node at which the block occurred, the consent scopes that were not granted (codes, never patient data), and a support-routing hint. The stop-screen copy is a presentation concern (the intake kit renders it); no patient data ever rides here. No order lock ever forms without a positive affirmation where one is required.
Show child attributes
Show child attributes
Post-completion hand-off carried when a session that began from an entry link completes. Present only when status is complete and the session carried a validated entry context. redirect holds a brand-allowlisted https URL; promo is an opaque pass-through token validated downstream; test is the per-client test-mode flag. Never carries patient data.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
A node as presented to the client — PRESENTATION ONLY. The client never owns the sequence, controls, or option order (those come from the compiled bundle; a brand theme cannot alter them). Brand theme/copy, when applied, ride alongside as presentation overlay.
Show child attributes
Show child attributes
Server-computed progress metadata, present on every resolve/next step: computed from the live effective plan — never client-guessed. position_estimate is the honest fraction of the plan completed (0..1); display nodes participate in the fraction. Whether and how it renders (thin persistent bar, step ring, interstitial pacing, none) is brand-theme presentation.
Show child attributes
Show child attributes