Show or hide questions, and branch respondents after a question.
Values for choice questions are option ids: read them from config.options[].id on List questions. Rules can only be written while a survey is a draft. Show and hide need no further entitlement; branches need advanced logic, which every plan with API access includes. A rule is checked exactly like one built on the Logic page. Every logic endpoint also needs the questions:read scope, because a rule's sentence quotes question titles and answer labels.
/surveys/:id/logicEvery show, hide and branch rule on the survey, each with a plain-English sentence. Readable on every plan with API access.
A survey with calibration arms holds a copy of every question and rule per arm. Each rule's variantId names the arm whose questions it is on (null for the original survey), as each question's does on GET /surveys/:id. Changing one copy changes that arm only.
{
"surveyId": "SURVEY_ID",
"rules": [
{
"id": "RULE_ID",
"kind": "show",
"questionId": "Q5_ID",
"match": "all",
"conditions": [
{ "source": { "type": "question", "questionId": "Q3_ID" }, "operator": "is_any", "value": ["yes"] }
],
"active": true,
"sentence": "Show Q5 What did you like? when Q3 Did you buy? is Yes",
"variantId": null
}
]
}surveys:readquestions:read/surveys/:id/logicAdd one rule. kind is show, hide or branch: show and hide govern questionId; a branch runs after it and needs then ({ "goTo": QUESTION_ID }, { "end": true }, { "disqualify": true, "message"? } or { "redirect": URL }). Condition sources are a question, metadata or a url_parameter.
POST /surveys/:id/logic
{
"kind": "show",
"questionId": "Q5_ID",
"match": "all",
"conditions": [
{ "source": { "type": "question", "questionId": "Q3_ID" }, "operator": "is_any", "value": ["yes"] }
]
}
→ 201
{ "id": "…", "kind": "show", "questionId": "Q5_ID", "match": "all", "conditions": […], "active": true,
"sentence": "Show Q5 What did you like? when Q3 Did you buy? is Yes" }400 when a rule can't run:
{ "error": { "code": "invalid_request", "message": "Pick an answer from Q3's options",
"details": { "problems": [{ "code": "RULE_INVALID", "message": "Pick an answer from Q3's options",
"questionId": "Q5_ID", "field": "value", "conditionId": "c0" }] } } }
400 on a survey that is not a draft:
{ "error": { "code": "invalid_request", "message": "…", "details": { "reason": "not_draft" } } }conditionId is c0, c1, … : the condition's place in conditions. The problem has the same shape a blocked publish returns. A branch on a question inside a loop that isn't the loop's last question is refused with field: "question", as a loop never runs it: add it to the loop's last question instead, where it runs once after the loop. A condition's value is at most 20,000 characters serialised and a list at most 1,000 entries. A rule's request body is at most 4.5 MB, the hosting platform's own limit: a larger one is 413 payload_too_large. Only a rule near those sizes in a three-byte script (Chinese, Japanese, Korean) or with non-ASCII escaped as \uXXXX reaches it.
surveys:writequestions:read/surveys/:id/logic/:ruleIdReplace a rule whole, with the same body as Create. It is checked again and keeps its id, with one exception: a legacy standalone rule (saved as a single row before rules were grouped) is converted on update and gets a new id, returned in the response. A rule id that is not one of this survey's show, hide or branch rules is 404.
surveys:writequestions:read/surveys/:id/logic/:ruleIdRemove a rule. Returns 204 No Content. Draft surveys only, with no plan gate: an organisation that has moved to a plan without branching can still remove a branch.
surveys:writequestions:readDELETE /surveys/:id/questions/:questionId on a question logic depends on answers 400 with details: { "reason": "referenced_by_logic", "ruleIds": […], "rules": [{ "id", "kind", "effect", "sentence"? }], "variables": [{ "id", "name" }] }. Resend with ?acknowledgeLogic=RULE_ID,RULE_ID naming every listed rule to delete the question. A rule with effect: "deleted" is on another question that reads or targets this one, and is deleted with it in the same write. A rule with effect: "stops" is one of the question's own set_variable rules: it sets its variable for every respondent, and stops when the question goes. Each rule's kind is show, hide, branch or, for a rule this API does not list (such as a calculated field's set_variable), its own action. The sentence is included only for a key with questions:read. variables names the calculated fields that read the question's answer: they will be empty for new respondents, and need no acknowledgement. A delete that goes ahead with such fields answers 200 with { "warnings": { "variables": [{ "id", "name" }] } } instead of 204. A question a loop, quota, screener, conditional thank-you, randomisation set or recorded history depends on is refused first, with details.reason: "delete_blocked", and nothing is deleted.