A survey definition is a JSON document. The dashboard editor, the REST API, the MCP server and Kai all read and write this format, and Gleap validates it the same way everywhere.
{
"schemaVersion": 2,
"format": "card",
"defaultLanguage": "en",
"welcome": { … },
"blocks": [ … ],
"endings": [ … ],
"fields": [ … ],
"showProgress": true,
"allowSkipBack": true
}Top level
schemaVersion 2
Always 2.
format 'card' | 'full' (required)
card (compact card, bottom sheet on phones) or full (Typeform-style full screen). The legacy values survey and survey_full are deprecated aliases of card and full.
defaultLanguage string (required)
Locale of the default texts, for example en.
welcome Welcome
Optional welcome screen before the first question — see Welcome screen.
blocks Block[] (required)
The questions in display order. 1–60 blocks.
endings Ending[] (required)
Thank-you screens. 1–10 endings.
fields HiddenField[]
Hidden fields: values passed in by the SDK, URL parameters or personal links.
showProgress boolean
Show the progress bar.
allowSkipBack boolean
Full screen: allow going back to earlier questions.
theme Theme
The survey's design — see Theme. Every field is optional; unset fields use your widget settings.
sender object
{ userId? } — the teammate shown in the card's sender row. Defaults to the project's default sender.
Localized texts
Every text (title, description, choice label, …) is a map of locale to text:
{ "en": "How likely are you to recommend us?", "de": "Wie wahrscheinlich ist es, dass Sie uns weiterempfehlen?" }Titles may contain tokens that are replaced for each respondent: {{contact.first_name}}, {{field.<hidden field key>}} and {{answer.<earlier answer key>}}.
Theme
theme sets the survey's design. Every field is optional: a field you leave out uses your project's widget settings (widget colour, widget logo, widget light/dark setting), so a survey without theme looks like your widget.
brandColor hex
Buttons, selected choices and the progress bar. Default: the widget colour.
buttonTextColor hex
Text on brand-filled buttons and selected choices. Default: black or white, whichever contrasts better with brandColor.
appearance 'auto' | 'light' | 'dark'
Light or dark surfaces. auto (default) follows the widget's light/dark setting.
background 'tint' | 'plain' | 'color' | 'image'
The backdrop of the full-screen format and the survey page. tint (default) is a light tint of the brand colour, plain has no tint, color uses backgroundColor, image uses backgroundImage. The card and the bottom sheet always stay a solid light or dark surface.
backgroundColor hex
Backdrop colour for background: "color".
backgroundImage string
Backdrop image for background: "image". Must be an https URL without quotes, parentheses or whitespace.
backgroundOverlay number
0–0.8. Black darkening over a colour or image backdrop so text stays readable. Default 0.3 for images, 0 for colours.
textColor hex
Text on the full-screen and survey page backdrop. Default: picked by contrast — light text on dark colours and images.
corners 'sharp' | 'rounded' | 'pill'
Corner radius of choices, buttons and inputs. Default rounded.
logoUrl string
https URL of the logo. Default: the widget logo.
showLogo boolean
Show the logo — top left in the full-screen format and in the survey page header. Default true.
Where each field applies:
Card / bottom sheet | Full screen | Survey page | |
|---|---|---|---|
| ✓ | ✓ | ✓ |
| — | ✓ | ✓ |
| — | ✓ | ✓ |
A brand colour, a photo backdrop darkened for legibility and pill-shaped choices:
"theme": {
"brandColor": "#0f766e",
"background": "image",
"backgroundImage": "https://cdn.example.com/surveys/forest.jpg",
"backgroundOverlay": 0.4,
"corners": "pill"
}Colours are hex (#0f766e, #fff, or with alpha). An invalid backgroundImage or backgroundColor falls back to tint.
Resolved theme
The SDK and the survey page load a survey through GET /surveys/{id} and GET /surveys/page/{slug}. Both return theme with the widget defaults already filled in:
"theme": {
"brandColor": "#0f766e",
"appearance": "auto",
"autoAppearance": "system",
"background": "image",
"backgroundImage": "https://cdn.example.com/surveys/forest.jpg",
"backgroundOverlay": 0.4,
"corners": "pill",
"showLogo": true,
"logoUrl": "https://cdn.example.com/logo.png",
"widgetBackgroundColor": "#ffffff",
"buttonColor": "#0f766e",
"defaults": { "brandColor": "#485bff", "appearance": "system", "logoUrl": "https://cdn.example.com/logo.png", "backgroundColor": "#ffffff" }
}Field | |
|---|---|
| Always present, defaults applied. |
| What |
| Only when the survey sets them; otherwise pick by contrast. |
| Only for a |
| The survey logo, else the widget logo; absent when |
| The widget's own colours, when set. |
| The widget values unset fields fall back to: |
Blocks
id string (required)
Stable id, unique across blocks and endings (letters, digits, _, -; max 40). Jumps point to it.
key string (required)
Stable answer key, unique, ^[a-z][a-z0-9_]{1,40}$ — for example nps_score. Webhooks, exports, the API and filters use it. Don't rename it on a live survey.
type string (required)
One of the types below.
title Localized (required)
The question.
description Localized
Help text under the question.
required boolean (required)
Whether the question must be answered. statement blocks are never required; a required consent must be ticked.
defaultValue string | number | boolean | string[]
Preselected answer the question starts with: a choice id (single), choice ids (multi), a scale value, true / false (yesno) or text. The respondent still confirms it; nothing is saved before. Must be a valid answer of the block (invalid_default otherwise).
jumps Jump[] (required)
Logic rules, checked top to bottom; the first match wins. Max 20. [] for none.
next string | null (required)
"All other cases go to": a later block id or an ending id. null = the next block in order (after the last block, the first ending).
Type-specific options:
Type | Answer value | Options |
|---|---|---|
| number 0–10 |
|
| number 1–5 |
|
| number |
|
| number |
|
| choice id, or |
|
| array of choice ids (plus |
|
|
| — |
| string |
|
| string |
|
| number |
|
|
| — |
| array of file URLs |
|
| — (no answer) | — |
|
|
|
Choices are { "id": "too_expensive", "label": { "en": "Too expensive" } }. Choice ids are unique within the block; answers and conditions use the id, never the label. Any block can also carry media: { type: 'image' | 'video', url }.
Logic: jumps and conditions
A jump is { "if": <condition>, "go": "<block id or ending id>" }. go must point to a later block or to an ending — jumps never go backwards, so there are no loops.
Where a person goes after answering:
the first jump whose condition matches,
otherwise
next,otherwise the next block in order,
after the last block, the first ending.
Conditions on the answer
Condition | Use with |
|---|---|
|
|
| same as |
|
|
|
|
| any optional question |
Conditions on the person
Condition | Meaning |
|---|---|
| Contact property. |
| Hidden field (must be listed in |
| Page URL the survey is shown on. |
The editor lists person conditions before answer conditions; put them first when you write definitions yourself.
Welcome screen
welcome adds an intro screen before the first question, like Typeform's. It is not a block: it has no answer key, no logic and does not count as a question. In full screen and on the survey page it shows centred, with the optional image, a large title, the description, the Start button ("press Enter ↵") and the time estimate. The card and the phone bottom sheet show a compact version (title, description, Start). People who come back to finish a response they started skip it.
enabled boolean (required)
true shows the welcome screen. false keeps the texts without showing them.
title Localized (required)
The heading. Required when enabled is true; may contain tokens like {{contact.first_name}}.
description Localized
One or two sentences under the title.
buttonLabel Localized
Start button text. Default "Start".
media object
{ type: "image", url } — an image above the title (full screen and survey page). https only.
showTimeToComplete boolean
Show "Takes about N minutes" (about 15 seconds per question). Default true.
"welcome": {
"enabled": true,
"title": { "en": "Help shape Acme" },
"description": { "en": "Three quick questions. Your answers go straight to the product team." },
"buttonLabel": { "en": "Let's go" },
"media": { "type": "image", "url": "https://cdn.acme.com/survey-hero.png" }
}Use the welcome screen rather than a leading statement block: question numbers, the step funnel and "starts" (first answer) stay the same, and the analytics report how many people pressed Start.
Endings
id string (required)
Unique across blocks and endings.
title Localized (required)
Thank-you text.
description Localized
button object
{ label: Localized, url } — a call to action.
redirectUrl string
Survey page: redirect after this ending.
autoCloseSeconds number
In the app: close after N seconds.
tagContact string
Tag added to the contact who reaches this ending.
Hidden fields
"fields": [{ "key": "plan", "label": "Plan" }, { "key": "source" }]Values come from showSurvey(id, { fields }), URL parameters on the survey page (?plan=pro) and personal links. They are stored on the response under fields.
Validation
Gleap rejects a definition at publish time when:
ids or keys are missing, malformed or duplicated (ids are unique across blocks and endings; choice ids within a block),
a
goornextpoints to an unknown id or to an earlier block (loop),a block can't be reached from the first block, or a path doesn't end in an ending,
a condition doesn't fit its block type (for example
includeson asingleblock,between: [0, 11]on NPS, or a value condition on aconsent),scaleStyle: 'compact'is set on anything but NPS, or adefaultValueis not a valid answer of its block,a limit is exceeded: 60 blocks, 10 endings, 25 choices per block, 20 jumps per block.
Saving a draft accepts invalid logic and returns the problems as warnings; publishing returns 422 with the same list. Use POST /validate to check a definition without saving. Errors look like:
{ "ok": false, "errors": [{ "path": "blocks.2.jumps.0.go", "code": "backward_jump", "message": "\"q_reason\" is not after this block" }] }Example: NPS with follow-ups
Detractors (0–6), passives (7–8) and promoters (9–10) each get their own follow-up question.
{
"schemaVersion": 2,
"format": "card",
"defaultLanguage": "en",
"blocks": [
{
"id": "q_nps", "key": "nps_score", "type": "nps", "required": true,
"title": { "en": "How likely are you to recommend us to a friend?" },
"lowLabel": { "en": "Not likely" }, "highLabel": { "en": "Very likely" },
"jumps": [
{ "if": { "src": "answer", "op": "between", "value": [0, 6] }, "go": "q_detractor" },
{ "if": { "src": "answer", "op": "between", "value": [7, 8] }, "go": "q_passive" },
{ "if": { "src": "answer", "op": "between", "value": [9, 10] }, "go": "q_promoter" }
],
"next": null
},
{ "id": "q_detractor", "key": "detractor_reason", "type": "long", "required": false,
"title": { "en": "Sorry to hear that. What disappointed you?" }, "jumps": [], "next": "end_thanks" },
{ "id": "q_passive", "key": "passive_improve", "type": "long", "required": false,
"title": { "en": "What would make it a 10?" }, "jumps": [], "next": "end_thanks" },
{ "id": "q_promoter", "key": "promoter_love", "type": "long", "required": false,
"title": { "en": "Great! What do you love most?" }, "jumps": [], "next": "end_thanks" }
],
"endings": [{ "id": "end_thanks", "title": { "en": "Thanks for your feedback!" } }],
"fields": [],
"showProgress": true,
"allowSkipBack": true
}Example: cancellation survey
People who pick "Too expensive" are asked what price would have worked; everyone else goes straight to "What could we have done better?". People who agree to be contacted are tagged.
{
"schemaVersion": 2,
"format": "full",
"defaultLanguage": "en",
"blocks": [
{
"id": "q_reason", "key": "cancel_reason", "type": "single", "required": true,
"title": { "en": "What's the main reason you're leaving?" },
"choices": [
{ "id": "too_expensive", "label": { "en": "Too expensive" } },
{ "id": "missing_features", "label": { "en": "Missing features" } },
{ "id": "switching", "label": { "en": "Switching to another tool" } },
{ "id": "not_using", "label": { "en": "Not using it enough" } }
],
"allowOther": true,
"jumps": [{ "if": { "src": "answer", "op": "eq", "value": "too_expensive" }, "go": "q_price" }],
"next": "q_better"
},
{ "id": "q_price", "key": "fair_price", "type": "number", "required": false,
"title": { "en": "What monthly price would have worked for you?" }, "jumps": [], "next": "q_contact" },
{ "id": "q_better", "key": "do_better", "type": "long", "required": false,
"title": { "en": "What could we have done better?" }, "jumps": [], "next": null },
{
"id": "q_contact", "key": "may_contact", "type": "yesno", "required": true,
"title": { "en": "May we contact you about this, {{contact.first_name}}?" },
"jumps": [{ "if": { "src": "answer", "op": "eq", "value": true }, "go": "end_contact" }],
"next": "end_bye"
}
],
"endings": [
{ "id": "end_contact", "title": { "en": "Thanks — we'll be in touch." }, "tagContact": "churn-followup" },
{ "id": "end_bye", "title": { "en": "Thanks for being a customer." } }
],
"fields": [],
"showProgress": true,
"allowSkipBack": true
}Responses
Answers are stored keyed by the block's key:
{
"id": "rsp_4kq9X2mT7vB1nR8sLp3Z",
"surveyId": "q7x2kd",
"version": 3,
"status": "completed",
"answers": { "cancel_reason": "too_expensive", "fair_price": 49, "may_contact": true },
"path": ["q_reason", "q_price", "q_contact", "end_contact"],
"endingId": "end_contact",
"channel": "web",
"fields": { "plan": "pro" },
"language": "en",
"startedAt": "2026-10-06T09:12:03.000Z",
"completedAt": "2026-10-06T09:12:41.000Z"
}channel is one of web, ios, android, page, email, embed, conversation, api.

