Surveys and delivery rules
Collect survey responses and configure where your product shows each survey.
A survey collects answers to a questionnaire you define. Delivery rules describe where and when your product should show it. Support stores both; your application renders the questions and enforces visitor-specific rules.
Responses are separate from support conversations: they have no thread status, messages, assignment, or workspace notification. Escalate a response when your team needs a conversation to follow up.
The examples use a first-week survey for your shop. Replace ids, customer details, URLs, and targeting values with your own. Question and option ids belong to your application and must agree across the survey and its answers.
Lifecycle
A survey is in exactly one of three states:
State | Editable | Answerable |
|---|---|---|
draft | Yes | No |
published | No | Yes |
archived | No | No |
support_survey_publish validates a draft and opens it for responses. Published questions cannot be edited. Answers refer to question ids, so keep each id associated with the same question meaning.
support_survey_unpublish returns a published survey to draft for editing and stops new responses immediately. Existing responses remain. The call rejects surveys that are not published.
support_survey_archive permanently archives a draft or published survey. Existing responses remain readable, but the survey cannot return to draft or accept answers.
Create always starts a draft. Publish separately after it passes publication validation.
The survey object
{
"id": "9f3c1b02-77ad-4e51-b0c9-5a8e2d4f6017",
"name": "After your first week",
"status": "published",
"questions": [
{
"id": "q_channel",
"type": "radio",
"label": "How did you get started?",
"required": true,
"options": [
{ "id": "docs", "label": "Reading the documentation" },
{ "id": "sales", "label": "A call with our team" },
{ "id": "self", "label": "I worked it out myself" }
],
"skip": { "self": "q_blockers" }
},
{
"id": "q_score",
"type": "nps",
"label": "How likely are you to recommend this shop?"
},
{
"id": "q_blockers",
"type": "checkbox",
"label": "What got in your way?",
"options": [
{ "id": "billing", "label": "Billing" },
{ "id": "permissions", "label": "Account permissions" }
]
}
],
"createdAt": "2026-08-20T09:14:02.331Z",
"updatedAt": "2026-08-20T09:20:47.882Z"
}name must contain 1–255 characters. It becomes the title of a feedback thread when you escalate a response, so use a name your team can recognize in the queue.
questions holds up to 100 questions in display order. support_survey_update replaces the entire set you send. Pass [] to empty a draft.
support_survey_create creates the draft. support_survey_list and support_survey_get read surveys in any state. Creating and editing require product write access; publishing, unpublishing, and archiving require admin access; reads and statistics require read access.
Questions
Give each question a unique, stable id within the survey. Its label is the text the respondent reads. Answers use ids, so reordering questions does not change which answer belongs to which question.
Type | What it collects | Answer shape |
|---|---|---|
reaction | A free string your client owns — an emoji, typically. | String |
text | Free prose. | String |
radio | One option. | An option id |
checkbox | Any number of options. | An array of option ids |
rating | An integer on a declared scale. | Integer |
nps | The recommendation question, fixed to 0–10. | Integer |
statement | Nothing. It is displayed, never answered. | None — never send one |
Use these properties only with the supported question type. An incompatible property returns 400 with field errors:
options—radioandcheckboxonly. Each option is anidand alabel, both client-owned.scale—ratingonly.minis0or1;maxis an integer from 2 to 10. The default is{ "min": 1, "max": 5 }.npsis fixed at 0–10 and accepts no scale.skip—radioonly. See Skip logic.
required is enforced when completed is true. Partial responses can omit required answers. A statement cannot be required because it collects no answer.
{
"id": "q_channel",
"type": "radio",
"label": "How did you get started?",
"required": true,
"options": [
{ "id": "docs", "label": "Reading the documentation" },
{ "id": "sales", "label": "A call with our team" },
{ "id": "self", "label": "I worked it out myself" }
],
"skip": { "self": "q_blockers" }
}Skip logic
On a radio question, skip maps an option id to a later question id or "end". Other question types cannot use it.
Targets must come later in the question list or end the survey. Backward jumps and loops are rejected at publication.
Omit skipped and unanswered questions from answers; do not send a special skipped value. The response does not distinguish a question that was hidden from one left blank.
What publication checks
Draft saves validate each field’s shape. Publication also checks relationships between questions. support_survey_publish requires:
- it has at least one question;
- question ids are unique, and so are the option ids within a question;
- every
radioandcheckboxquestion offers options; - no
statementis marked required; - every
skipnames an option the question actually offers, and targets a later question orend.
These checks ensure the published skip path can reach the end without looping.
Answers
answers maps question ids to values of the corresponding type. An unknown question id, an invalid value, or an answer to a statement returns 400.
Escalate a response
support_response_escalate creates a feedback thread from a response. It uses the survey name as the title, the same external respondent as the author, the default open status, and the response metadata plus surveyResponseId. If the response’s score fits 0–10, it becomes the thread’s rating.
The new thread starts without messages; the answers stay on the response.
Escalating the same response again returns a conflict identifying the existing thread instead of creating another one.
Escalation requires product write access through the authenticated API. It is not available with a public key.
Read responses back
A stored response includes its answers, derived score, completion flag, and any escalation id:
{
"id": "3a7c5e91-2d48-4b06-9f13-8e5a0c7d2b64",
"surveyId": "9f3c1b02-77ad-4e51-b0c9-5a8e2d4f6017",
"authorExternalId": "user_8842",
"answers": { "q_channel": "docs", "q_score": 9 },
"score": 9,
"completed": true,
"metadata": { "locale": "en-GB" },
"escalatedThreadId": null,
"createdAt": "2026-08-20T11:31:55.204Z",
"updatedAt": "2026-08-20T11:31:55.204Z"
}score contains the raw answer from the first nps or rating question, or null when neither was answered. It is not rescaled. Keep 0 distinct from null.
Read the score with its source question: a 1–5 rating and a 0–10 NPS answer use different scales even though both appear in score.
support_response_list requires surveyId and filters by inclusive scoreMin / scoreMax, creation window from / to, and completed. Unscored responses match neither score bound. support_response_get reads one response.
support_survey_stats returns response and completion counts for a time window. Per-question results include option counts for radio and checkbox, means and distributions for rating and nps, and answered counts for other types. For the first NPS question it also returns promoters (9–10), passives (7–8), detractors (0–6), and the NPS score.
The default window is the last 30 days. Set from explicitly for older results; omitting it does not request all history.
The two public survey routes
Your backend can read published questions and submit responses through two public survey routes. Survey management, collected-response reads, statistics, and escalation require the authenticated API; their paths return 404 on the public API.
Both routes use the product public key and base path in Public HTTP API, with its error shapes, CORS rules, and rate limit. Keep the key on your backend; the browser calls your backend.
Access: no tokens
Public survey requests require a product public key and surveyId. There is no additional respondent token, signed link, expiry, or single-use guard.
surveyId identifies the survey and does not authorize a respondent. Support stores the authorExternalId your backend asserts without checking the person, invitation, or previous responses. It neither deduplicates responses nor applies per-respondent rate limits.
Your backend decides who may answer and how often.
Reading and answering
GET /surveys/{surveyId} returns a published survey and its questions. Draft, archived, unknown, and other-product surveys all return the same 404. Check the status through the authenticated API when diagnosing a not-found response.
POST /surveys/{surveyId}/responses creates one response to the survey in the path. It is the only public survey write.
{
"authorExternalId": "user_8842",
"completed": true,
"answers": {
"q_channel": "docs",
"q_blockers": ["billing", "permissions"],
"q_score": 9
},
"metadata": {
"pageUrl": "https://shop.example.com/welcome",
"locale": "en-GB"
}
}Field | Rules |
|---|---|
authorExternalId | Required. 1–255 characters. Your own identifier for the respondent, stored verbatim and never resolved, normalized or enriched. |
answers | Required. Keyed by question id, each value fitting that question’s type. A skipped or blank question is absent. |
completed | Required. true when the respondent reached the end, false for a partial response. Required questions are enforced only when it is true. |
metadata | Optional. Free-form JSON of your own, stored verbatim and never read. |
This request has a 256 KiB body cap, shared with POST /messages. Other public routes allow 64 KiB.
Send completed: false for a partial response and true when the respondent reaches the end. Filter by completion when analyzing results. Each submission creates a response; sending a completed response after a partial one does not update the earlier record.
Submitting to a draft, archived, unknown, or other-product survey returns 404 without revealing which condition applies.
The result is the response object shown above, with escalatedThreadId: null. Fields are at the top level; there is no { "success": true, "data": … } wrapper. Read body.answers, for example, rather than body.data.answers.
Response submission has no MCP tool or authenticated API operation. It records a person’s answers from your product through the public API. A request carrying a Lepsto identity is rejected before the handler with 401 and error.code: "public_edge_only"; no response is created. Granting more platform permissions does not enable this route.
Delivery rules
A delivery rule belongs to one survey and specifies page matching, device targeting, a trigger, frequency, and visitor conditions.
Your application executes the rule. Support has no scheduler, widget, impression log, or per-visitor history. It returns matching configuration; your runtime decides whether and when to show it. See The client-execution contract.
The rule object
{
"id": "3f0a5c11-6b2d-4f8e-9a17-2c4e8d3b5a90",
"surveyId": "9f3c1b02-77ad-4e51-b0c9-5a8e2d4f6017",
"enabled": true,
"urlMatch": { "kind": "prefix", "value": "https://shop.example.com/checkout" },
"device": "mobile",
"trigger": { "kind": "delay", "seconds": 15 },
"frequency": { "kind": "until_answered", "cooldownDays": 7 },
"attributeConditions": [
{ "key": "plan", "op": "eq", "value": "pro" },
{ "key": "trial_ends_at", "op": "exists" }
],
"createdAt": "2026-08-20T09:14:02.118Z",
"updatedAt": "2026-08-20T09:14:02.118Z"
}Field | Rules |
|---|---|
id | The rule’s own id. |
surveyId | The survey this rule delivers. Required at creation, must belong to your product, and not editable afterwards. |
enabled | Whether the rule is live. A rule is created enabled unless you say otherwise. |
urlMatch | Required. Which pages the rule applies to. |
device | any, desktop or mobile. Defaults to any. |
trigger | Required. When to show the survey once the page matches. |
frequency | Required. How often the same person may see it. |
attributeConditions | Conditions on the visitor, all of which must hold. Defaults to an empty list, which applies to everyone. |
createdAt, updatedAt | ISO-8601 timestamps. |
You can create rules for a draft survey. They remain hidden from the public API until the survey is published.
URL matching
urlMatch is a kind and a value, and the value is at most 512 characters.
kind | Matches when |
|---|---|
exact | The URL is the value, character for character. No case folding and no trailing-slash forgiveness. |
prefix | The URL starts with the value. |
contains | The value appears anywhere in the URL. |
regex | A JavaScript regular expression, applied unanchored. |
Regular expressions are unanchored: /checkout matches anywhere in the URL. Add ^ and $ when the match must cover the whole string.
An invalid regular expression returns 400 when you save the rule. Literal exact, prefix, and contains values are not compiled, so a string such as ([ is accepted literally.
Devices
device: "any" is the default and matches either reported device.
When requesting rules, report desktop or mobile; any is a rule setting, not a valid reported device. Omitting the device returns only any rules.
Triggers
Each trigger accepts only the fields for its kind. An extra field, such as seconds on immediate, returns 400.
kind | Field | Means |
|---|---|---|
immediate | — | Show it as soon as the page matches. |
delay | seconds, an integer from 0 to 86,400 | Wait this long after the page matched, at most a day. |
event | name, 1–128 characters | Wait for an event in your own runtime. |
Named events belong to your runtime. Support stores the name without checking an event registry or listening for it.
Frequency
frequency is a kind and an optional cooldownDays, an integer from 1 to 365.
kind | Means |
|---|---|
once | Show it a single time, ever. |
until_answered | Keep showing it until the person responds. |
always | Show it every time the rule matches. |
once does not accept cooldownDays; sending both returns 400.
Your runtime enforces frequency and keeps the visitor’s impression and response history. Support stores only the policy.
Attribute conditions
A rule accepts up to 20 attribute conditions. Every condition must match (AND); OR conditions are not supported.
op | Holds when | value |
|---|---|---|
eq | The attribute equals value | Required |
neq | The attribute does not equal value | Required |
contains | value appears in the attribute | Required |
exists | The attribute is present at all | Refused |
Keys allow up to 128 characters and values up to 512. You define their meaning. Support stores them verbatim without looking up visitors or validating the vocabulary.
An empty condition list applies to everyone. Send attributeConditions: [] to clear it. Your runtime evaluates the conditions against visitor attributes.
Managing rules
Tool | REST | What it does |
|---|---|---|
POST /support/rules | Write a rule against one survey | |
GET /support/rules/{id} | One rule, with its whole configuration | |
GET /support/rules | This product’s rules, newest first | |
PATCH /support/rules/{id} | Edit a rule, including switching it off | |
DELETE /support/rules/{id} | Remove a rule for good |
Rule creation and updates require product write access, reads require read, and deletion requires admin. These tools use platform authentication. The public API only allows the matching-rule read.
Updates replace each supplied field in full, including attributeConditions and urlMatch, and require at least one field. surveyId cannot change: delete the rule and create another to target a different survey. Set enabled: false to stop delivery reversibly without changing the survey.
support_rule_delete permanently removes the rule and leaves its survey untouched. A second delete returns 404. Rules have no archive or restore operation.
support_rule_list returns { "rules": [...], "limit": …, "offset": … }, newest first. Its filters:
Filter | Effect |
|---|---|
surveyId | Only rules delivering this survey |
enabled | true for live rules, false for switched-off ones |
url | Only rules whose URL match applies to this page URL |
device | desktop or mobile; without it only any rules match |
limit, offset | Page size 1–100 (default 25) and how many to skip |
Both authenticated and public reads support URL and device matching. An authenticated read with neither filter returns all rules. Pagination runs after matching, so the page size applies to matched results.
Authenticated reads include rules for surveys in any state, allowing you to configure them before publication.
The rule read at the public edge
The public API exposes GET /rules with these restrictions:
Authenticated | Public edge | |
|---|---|---|
url | Optional | Required |
enabled | Filter of your choosing | Forced to true; the filter is not honored |
Survey status | Any | published only |
Send the page’s url; omitting it returns 400. Support applies URL and device matching before returning rules.
Public reads return only enabled rules for published surveys. Unpublishing the survey hides all its rules; disabling one rule hides only that rule.
Public POST /rules, PATCH /rules/{id}, and DELETE /rules/{id} return 404. Use the authenticated API to change rules.
The public read returns the complete matching rule, including its conditions. It does not redact fields.
The client-execution contract
Support selects rules for the page. Your runtime applies the visitor-specific checks and runs the trigger.
Support decides:
- which rules belong to the product,
- whether the rule is
enabledand its surveypublished, - whether
urlMatchapplies to theurlyou sent, - whether
deviceadmits the device you reported.
Your runtime decides:
- whether
attributeConditionshold, against attributes only you have; - when to show the survey, honoring
trigger— now, afterseconds, or on your own named event; - whether this person may be shown it at all, honoring
frequencyandcooldownDaysagainst the per-visitor history only you keep.
Use your backend for every Support request in this flow; never send the public key to the browser:
- On page load, send the current URL and device to your backend, which calls
GET /rules?url=…&device=…. - Discard rules whose
attributeConditionsdo not match the visitor. - Check
frequencyandcooldownDaysagainst your visitor history. - Start the trigger: render immediately, wait
seconds, or listen for your named event. - When triggered, fetch
GET /surveys/{surveyId}through your backend and render the survey. Record that it was shown. - Submit answers through your backend to
POST /surveys/{surveyId}/responsesand update your local response history.
Fetch rules on page load rather than polling. Recheck survey availability when fetching questions, since the survey may have been unpublished.
Rule errors
The shared error shapes are in Tools, SDK and errors. What is specific to rules:
Code | When |
|---|---|
400 | The body does not fit — a regex that does not compile, a match value over 512 characters, cooldownDays on once, a value on exists or missing on eq / neq / contains, an update with no field in it, or url missing at the public edge |
404 | The rule does not exist, or the rule or the survey belongs to another product |
Creating a rule for another product’s survey returns not found, without confirming that the survey exists.
If a stored regular expression no longer compiles, it matches nothing. Other rules can still be returned.
No webhook fires for a rule
Rule changes emit no webhooks. The seven supported events include survey publication and response creation, but no rule event.
Where to go next
- Public HTTP API: the key, the base path and the error shapes.
- Threads and messages: unsolicited feedback and the
ratingfield a response escalates into. - Tools, SDK and errors: every tool named here.