Appearance
Surveys API
Manage surveys, sections, and questions. All endpoints on this page are authenticated and scoped to the team that owns the API key.
Base URL: https://app.krafter.dev/api/v1
Authentication: Bearer token with surveys:read (read endpoints) or surveys:write (write endpoints). See API → Authentication.
For collecting responses, see Responses. For event delivery, see Webhooks. For aggregates, see Analytics.
The Survey object
| Field | Type | Description |
|---|---|---|
id | string | UUID |
name | string | Display name |
slug | string | URL-safe identifier — unique per team. Used in public response URLs (/api/v1/surveys/:slug/...) and embed config |
mode | string | manual (you author every question) or ai (the AI generates the next question per respondent) |
status | string | draft, active, or closed. Only active surveys accept responses |
settings | object | Free-form display settings (theme, button labels, redirect URL). Schema is owned by the embed widget |
ai_config | object | AI mode configuration (goal, target_questions, tone). Empty {} for mode: "manual" |
response_count | integer | Cached count of completed responses. Increments on complete_response |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |
GET /surveys/:id and POST /surveys/:id/duplicate additionally include sections and questions arrays (preloaded). List endpoints omit them.
List Surveys
GET /surveysRequired scope: surveys:read
Query parameters
| Param | Type | Description |
|---|---|---|
page | integer | 1-based page number. Default 1 |
per_page | integer | Items per page. Default 20, max 100 |
status | string | Filter by status (draft, active, closed) |
Example Request
bash
curl "https://app.krafter.dev/api/v1/surveys?status=active&page=1" \
-H "Authorization: Bearer kr_live_abc123def456"Example Response
json
{
"data": [
{
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Product feedback",
"slug": "product-feedback",
"mode": "manual",
"status": "active",
"settings": {},
"ai_config": {},
"response_count": 142,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-15T12:00:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 1,
"total_pages": 1
}
}Create Survey
POST /surveysRequired scope: surveys:write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
slug | string | Yes | URL-safe identifier, unique per team |
mode | string | No | manual (default) or ai |
settings | object | No | Display/widget settings |
ai_config | object | No | Required when mode: "ai" — at minimum {"goal": "..."} |
The survey is created with status: "draft". Use POST /surveys/:id/activate to start collecting responses.
Example Request
bash
curl -X POST https://app.krafter.dev/api/v1/surveys \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"name": "Product feedback",
"slug": "product-feedback",
"mode": "manual"
}'Example Response
json
// 201 Created
{
"data": {
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Product feedback",
"slug": "product-feedback",
"mode": "manual",
"status": "draft",
"settings": {},
"ai_config": {},
"response_count": 0,
"created_at": "2026-05-10T10:00:00Z",
"updated_at": "2026-05-10T10:00:00Z"
}
}Get Survey
GET /surveys/:idRequired scope: surveys:read
Returns the survey with sections and questions arrays preloaded.
Example Response
json
{
"data": {
"id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"name": "Product feedback",
"slug": "product-feedback",
"mode": "manual",
"status": "active",
"settings": {},
"ai_config": {},
"response_count": 142,
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-15T12:00:00Z",
"sections": [
{
"id": "s1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"title": "About you",
"description": null,
"position": 1,
"created_at": "2026-04-01T10:01:00Z"
}
],
"questions": [
{
"id": "q1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"section_id": "s1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"type": "text",
"title": "What did you build with Krafter?",
"description": null,
"options": [],
"settings": {},
"position": 1,
"branching": {},
"version": 1,
"created_at": "2026-04-01T10:02:00Z"
}
]
}
}Update Survey
PATCH /surveys/:idRequired scope: surveys:write
Send only the fields you want to change. name, slug, mode, status, settings, and ai_config are accepted.
Slug changes break public links
Changing slug invalidates existing embed configurations and respondent invitation URLs (/s/:slug). Prefer renaming via name only.
Example Request
bash
curl -X PATCH https://app.krafter.dev/api/v1/surveys/SURVEY_ID \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{"name": "Q2 product feedback"}'Delete Survey
DELETE /surveys/:idRequired scope: surveys:write
Permanently deletes the survey and all associated sections, questions, responses, respondents, webhooks, and summaries.
204 No ContentDuplicate Survey
POST /surveys/:id/duplicateRequired scope: surveys:write
Creates a new draft survey with the same sections, questions, settings, and ai_config. Responses, respondents, and webhooks are not copied. The new survey gets a fresh id, slug (suffixed to avoid collision), and response_count: 0.
Returns the new survey with sections and questions preloaded.
201 CreatedActivate Survey
POST /surveys/:id/activateRequired scope: surveys:write
Transitions status to active. The survey now accepts responses at POST /api/v1/surveys/:slug/responses and via the embed widget. No request body required.
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/activate \
-H "Authorization: Bearer kr_live_abc123def456"json
{ "data": { "id": "...", "status": "active", "...": "..." } }Close Survey
POST /surveys/:id/closeRequired scope: surveys:write
Transitions status to closed. New start_response requests are rejected. Existing in-progress responses can still complete. No request body required.
To reopen a closed survey, use POST /surveys/:id/activate.
List Templates
GET /surveys/templatesRequired scope: surveys:read
Returns the catalog of built-in survey templates (NPS, CSAT, CES, Product Feedback, Onboarding, Employee Engagement). Templates are starting points for the dashboard's "New from template" flow — there is no public "instantiate template" endpoint via the API. Build a survey from scratch with the standard create + add-questions flow.
json
{
"data": [
{ "id": "nps", "name": "Net Promoter Score", "description": "Single-question loyalty metric." },
{ "id": "csat", "name": "Customer Satisfaction", "description": "..." }
]
}Sections
Sections group questions into logical units. Sections are optional — a survey can have questions with section_id: null.
The Section object
| Field | Type | Description |
|---|---|---|
id | string | UUID |
survey_id | string | Owning survey |
title | string | null | Display title |
description | string | null | Optional helper text shown above questions |
position | integer | 1-based ordering within the survey. Required |
created_at | string | ISO 8601 timestamp |
List Sections
GET /surveys/:survey_id/sectionsRequired scope: surveys:read
Returns sections ordered by position.
Create Section
POST /surveys/:survey_id/sectionsRequired scope: surveys:write
| Field | Type | Required |
|---|---|---|
title | string | No |
description | string | No |
position | integer | Yes |
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/$SURVEY_ID/sections \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{"title": "About you", "position": 1}'201 CreatedUpdate Section
PATCH /surveys/:survey_id/sections/:section_idRequired scope: surveys:write
Accepts title, description, position. Send only the fields you want to change.
Delete Section
DELETE /surveys/:survey_id/sections/:section_idRequired scope: surveys:write
Questions belonging to the section are not deleted — their section_id is set to null. Reattach them to another section or leave them unsectioned.
204 No ContentQuestions
The Question object
| Field | Type | Description |
|---|---|---|
id | string | UUID |
survey_id | string | Owning survey |
section_id | string | null | Owning section, or null for top-level questions |
type | string | One of the 12 question types — see below |
title | string | The question text shown to respondents. Required |
description | string | null | Optional helper text shown beneath the title |
options | string[] | Choice labels for choice, multi_choice, ranking. Empty for free-text types |
settings | object | Type-specific config (e.g. {"min": 0, "max": 10, "labels": {...}} for scale) |
position | integer | 1-based ordering across the survey. Required |
branching | object | Conditional logic — see Branching |
version | integer | Incremented on edits to support response migration |
created_at | string | ISO 8601 timestamp |
Question types
| Type | Answer shape | Notes |
|---|---|---|
text | string | Single-line or paragraph (controlled by settings.multiline) |
email | string | Validated as RFC-5322 |
choice | string | Single selection from options[] |
multi_choice | string[] | Multiple selection from options[] |
rating | integer | 1-5 star rating |
nps | integer | 0-10 |
scale | integer | Numeric scale; bounds in settings.min / settings.max |
slider | integer | Continuous slider; bounds in settings.min / settings.max |
matrix | object | {row: column} map; rows in settings.rows, columns in settings.columns |
ranking | string[] | Ordered list of options[] values |
yes_no | boolean | true / false |
date | string | ISO 8601 date (YYYY-MM-DD) |
Branching
Each question may carry a branching object that tells the engine which question to ask next based on the answer:
json
{
"rules": [
{ "if": { "equals": "Yes" }, "then": { "go_to_question_id": "q-9c8b..." } },
{ "if": { "equals": "No" }, "then": { "skip_to_end": true } }
],
"default": { "go_to_next": true }
}Branching is enforced server-side when the respondent submits each answer. AI-mode surveys ignore branching — the AI engine picks the next question dynamically.
List Questions
GET /surveys/:survey_id/questionsRequired scope: surveys:read
Returns questions ordered by position.
Create Question
POST /surveys/:survey_id/questionsRequired scope: surveys:write
| Field | Type | Required |
|---|---|---|
type | string | Yes |
title | string | Yes |
position | integer | Yes |
section_id | string | No |
description | string | No |
options | string[] | Required for choice, multi_choice, ranking |
settings | object | Type-specific |
branching | object | No |
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/$SURVEY_ID/questions \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"type": "choice",
"title": "How did you hear about us?",
"options": ["Search", "Social", "Friend", "Other"],
"position": 2
}'201 CreatedUpdate Question
PATCH /surveys/:survey_id/questions/:question_idRequired scope: surveys:write
Send only the fields you want to change. Editing a question on an active survey increments version so existing responses remain valid against the prior shape.
Delete Question
DELETE /surveys/:survey_id/questions/:question_idRequired scope: surveys:write
Existing answers for this question are retained inside the response's answers map but are no longer rendered.
204 No ContentReorder Questions
POST /surveys/:survey_id/questions/reorderRequired scope: surveys:write
Atomically rewrites every question's position according to the supplied id order. Pass all question ids — partial lists return 400.
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/$SURVEY_ID/questions/reorder \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"question_ids": [
"q1c2d3e4-...",
"q2c2d3e4-...",
"q3c2d3e4-..."
]
}'json
{ "ok": true }Errors
Standard platform errors apply — see API → Errors. Surveys-specific notes:
404 Not Foundis returned for any survey, section, or question id that doesn't belong to your team — not403. The API never confirms the existence of resources outside your team.- Activating a survey with zero questions returns
422 Unprocessable Entity. - Slug collisions return
422witherrors.slug: ["has already been taken"].