Questionnaires
Intake questions live in a shared catalog. Load them, show a later question only when
an earlier answer matches, then submit the full set on the consult.
A visit can be opened first and the questionnaire attached later.
Once you send questionnaire, every question that applies must be answered
or the API returns 422 and stores nothing.
Load the form
GET https://bespoke-consults.designingit.co/v1/question-categories lists categories.
Send the category slug as questionnaire_type on the visit.
GLP-1 is glp-1. glp1 is accepted as the same category.
curl "https://bespoke-consults.designingit.co/v1/questions?category=glp-1" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
Each question includes key, label, type,
options, visible_when, validation, and config.
Use key as the answer id.
Which questionnaire
A category can contain more than one form. ED has sildenafil and tadalafil.
GLP-1 has one form, weight.
curl "https://bespoke-consults.designingit.co/v1/questions?category=ed&questionnaire=tadalafil" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json"
When you submit, send questionnaire.code if the category has more than one form.
A category with a single form does not need code.
Answer types
| Type | What to collect |
|---|---|
yes_no, single_select | One value from options. |
multi_select | An array of one or more values from options. |
yes_no_with_answer | One option. If config.requires_note_for matches, also send the note. |
short_text, long_text, number, date, weight | A typed value. date is YYYY-MM-DD. weight is pounds. |
height | {"feet": 5, "inches": 10} or 5'10. |
info | Show the label. Do not collect or send an answer. |
Conditional questions
visible_when is null on questions you always show.
When it is an object, show that question only after the rule matches an earlier answer.
Hide it otherwise, and do not send an answer for it.
The API does not require a hidden question. It does require it once the rule matches.
| Field | Meaning |
|---|---|
question_key | The earlier question key. |
operator | How to compare. See below. |
value | The answer to compare against. |
height_key | Used by bmi_below. Usually height. |
| Operator | Show the question when |
|---|---|
equals | The earlier answer equals value. For a multi-select, any selected value may match. |
not_equals | The earlier answer does not equal value. |
contains | A multi-select includes value, or a text answer contains it. |
in | The answer is one of the values in value (an array). |
not_in | The answer is not in that array. |
answered | The earlier question has any answer. |
not_answered | The earlier question has no answer. |
bmi_below | BMI from the weight answer and height_key is below value. |
A rule may also be {"all": [ ...rules ]} (every rule matches) or
{"any": [ ...rules ]} (one rule matches).
BMI is pounds × 703 / inches², rounded to one decimal.
Height in the BMI check is the height answer already collected.
GLP-1 follow-ups
The GLP-1 form (questionnaire_type: glp-1, code weight) always includes the health-history questions.
Three questions appear only after an earlier answer:
| Question | Show it when |
|---|---|
highest_weight |
BMI from pounds and height is under 30.
The answer itself must still produce a BMI of at least 30
(validation.min_bmi). Otherwise the API rejects it with
“Your BMI should be 30 or higher”.
|
glp1_allergy_contact |
glp1_allergy is Yes. Collect a phone number. |
glp1_current_medication |
glp1_current_use is Yes, currently taking.
Show the weekly milligram doses for the medication chosen in weight_product.
Semaglutide: 0.25, 0.5, 1, 1.7, 2.5 mg.
Tirzepatide: 2.25, 4.5, 6.75, 9, 11.25, 15 mg.
Always include the “I'm not sure…” option from config.extra_options.
config.options_by_answer is the same map.
|
glp1_allergy is No, and
glp1_current_use is No, leave those three questions off the payload.
Sending them is not required. Omitting a question that should be on screen is rejected.
list_medications is always shown. config.skip_answer is
I don't take any medications. That exact string is a complete answer.
weight_product is semaglutide or tirzepatide.
info screens (weight_loss_intro, weight_projection, qualified_for_treatment) are display only.
Extra text on an option
Some options set reveals_input. When the patient chooses that option and
required is true, collect the extra field (often “Please specify”) and send it with the answer.
On GLP-1, what_do_you_want does this for “I have another goal not listed above”.
{
"id": "what_do_you_want",
"answer": {
"values": ["Lose weight", "I have another goal not listed above"],
"inputs": {
"I have another goal not listed above": "Keep muscle while losing fat"
}
}
}
A single choice with extra text can be
{"value": "Yes", "input": "the explanation"}.
A yes_no_with_answer question uses the same shape when
config.requires_note_for matches the choice. note is accepted in place of input.
Submit answers
Send the questionnaire on POST https://bespoke-consults.designingit.co/v1/visits or later with
PUT https://bespoke-consults.designingit.co/v1/visits/{id}/questionnaire.
POST to that same path is an alias. Replacing overwrites the previous answers.
curl -X PUT "https://bespoke-consults.designingit.co/v1/visits/consult-1001/questionnaire" \
-H "X-Api-Key: $BESPOKE_KEY" \
-H "X-Api-Secret: $BESPOKE_SECRET" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"questionnaire_type": "glp-1",
"questionnaire": {
"code": "weight",
"type": "glp-1",
"answers": [
{ "id": "weight_product", "answer": "semaglutide" },
{ "id": "looking_to_lose_2", "answer": "21-50 lbs" },
{ "id": "weight_loss_barrier", "answer": "Low energy/fatigue" },
{ "id": "height", "answer": { "feet": 5, "inches": 10 } },
{ "id": "pounds", "answer": "220" },
{ "id": "what_do_you_want", "answer": ["Lose weight"] }
]
}
}'
The snippet above is not a full GLP-1 form. Every other non-info question
that is visible must be in answers as well. With height 5'10" and 220 lb,
BMI is about 31.6, so highest_weight stays hidden.
glp1_allergy: "No" hides the phone question.
glp1_current_use: "No" hides the dose question.
Required answers
The API accepts the questionnaire only when all of the following are true:
- Every visible question except
infohas an answer. - A select answer is one of that question’s
options(and, for the GLP-1 dose, a dose for the chosen medication or the “not sure” option). - A follow-up that
visible_whenhas turned on is present. - Required
reveals_inputtext is present when that option is chosen. - Numeric, height, date, and
validationrules pass, includinghighest_weightminimum BMI.
You can still POST /visits without a questionnaire and attach it later.
A payload that is a medication cart (requests or items) is not checked against the intake catalog.
Error response
A missing or invalid answer returns HTTP 422 with error.code
validation_failed. The consult is not updated.
error.fields["questionnaire.answers"] lists the missing keys.
Each questionnaire.answers.{key} field says what is wrong with that answer.
{
"error": {
"code": "validation_failed",
"message": "The submission could not be processed because some fields are invalid.",
"fields": {
"questionnaire.answers": [
"Answer every question that applies before submitting. Still missing: height, pounds, glp1_allergy_contact."
],
"questionnaire.answers.height": ["This question is required."],
"questionnaire.answers.pounds": ["This question is required."],
"questionnaire.answers.glp1_allergy_contact": ["This question is required."]
}
}
}
glp1_allergy_contact appears in that list only when glp1_allergy is Yes.
Fix the listed questions and submit the full questionnaire again.