Appearance
Analytics API
Aggregate stats, completion funnel, AI question generation, AI quality review, and AI summaries — all scoped to a single survey.
Base URL: https://app.krafter.dev/api/v1
| Endpoint | Purpose | Scope |
|---|---|---|
GET /surveys/:id/analytics | Overall stats + per-question breakdowns | surveys:read |
GET /surveys/:id/analytics/funnel | Drop-off by section | surveys:read |
POST /surveys/:id/generate | AI: generate draft questions for a goal | surveys:write |
POST /surveys/:id/review | AI: review existing questions | surveys:write |
GET /surveys/:id/summary | Latest AI summary | surveys:read |
POST /surveys/:id/summary | Enqueue a fresh AI summary | surveys:write |
The /generate, /review, and /summary endpoints depend on the AI engine. If it is not configured (no API key, model unreachable), they return 503 Service Unavailable. Transient upstream model errors return 502 Bad Gateway.
Get Analytics
GET /surveys/:id/analyticsRequired scope: surveys:read
Returns overall stats plus per-question breakdowns. All aggregations are computed in the database — there is no client-side fan-out.
Example Request
bash
curl https://app.krafter.dev/api/v1/surveys/SURVEY_ID/analytics \
-H "Authorization: Bearer kr_live_abc123def456"Example Response
json
{
"data": {
"stats": {
"total_responses": 250,
"completed": 200,
"in_progress": 30,
"abandoned": 20,
"completion_rate": 80.0,
"avg_duration_seconds": 92.5
},
"question_breakdown": {
"q1c2d3e4-...": {
"type": "choice",
"total": 200,
"counts": {
"Search": { "count": 100, "percentage": 50.0 },
"Social": { "count": 60, "percentage": 30.0 },
"Friend": { "count": 30, "percentage": 15.0 },
"Other": { "count": 10, "percentage": 5.0 }
}
},
"q2c2d3e4-...": {
"type": "numeric",
"count": 200,
"average": 8.4,
"min": 0,
"max": 10,
"distribution": { "0": 5, "5": 40, "8": 70, "9": 50, "10": 35 }
},
"q3c2d3e4-...": {
"type": "text",
"count": 187
}
}
}
}Stats fields
| Field | Type | Description |
|---|---|---|
total_responses | integer | All responses regardless of status |
completed | integer | Status completed |
in_progress | integer | Status in_progress |
abandoned | integer | Status abandoned (set by the abandonment cron) |
completion_rate | number | completed / total * 100, rounded to one decimal |
avg_duration_seconds | number | null | Avg completed_at − started_at across completed responses, rounded to one decimal. null when there are zero completed responses |
Question breakdown shapes
Aggregation runs only over completed responses, and is shaped per question type:
| Question type | Breakdown shape |
|---|---|
choice, multi_choice, yes_no | { type: "choice", total, counts: { <option>: { count, percentage } } } — multi_choice answers are unnested so each selected option contributes one count. yes_no reports "true" / "false" keys |
rating, nps, scale, slider | { type: "numeric", count, average, min, max, distribution } where distribution is { <value>: count } |
text, email, date, matrix, ranking | { type: "text", count } — only the count of non-empty answers (these types have no canonical aggregation today) |
Questions with zero answers still appear in question_breakdown with zero counts.
Get Funnel
GET /surveys/:id/analytics/funnelRequired scope: surveys:read
Counts how many responses reached each section of the survey. A response is considered to have "reached" a section when at least one question in that section has a saved answer in the response's answers map.
bash
curl https://app.krafter.dev/api/v1/surveys/SURVEY_ID/analytics/funnel \
-H "Authorization: Bearer kr_live_abc123def456"json
{
"data": [
{ "section_id": "s1c2d3e4-...", "title": "About you", "started": 250, "total": 250 },
{ "section_id": "s2c2d3e4-...", "title": "Your feedback", "started": 220, "total": 250 },
{ "section_id": "s3c2d3e4-...", "title": "Recommendation", "started": 200, "total": 250 }
]
}| Field | Type | Description |
|---|---|---|
section_id | string | Section UUID |
title | string | null | Section title (may be null for unnamed sections) |
started | integer | Responses with at least one answer in this section |
total | integer | All responses for the survey, regardless of status |
Sections are ordered by position. Surveys with no sections return an empty array — funnel analysis requires sections.
AI: Generate Questions
POST /surveys/:id/generateRequired scope: surveys:write
Asks the AI engine to draft a set of questions for a goal. This endpoint does not save anything — it returns a candidate question list you can review and then create via POST /surveys/:id/questions.
Request Body
| Field | Type | Description |
|---|---|---|
goal | string | What the survey is for. Defaults to the survey's name if omitted |
context | object | Optional. Audience, tone, locale, target length — anything the engine should consider |
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/generate \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"goal": "Understand why new signups churn before week 2",
"context": { "audience": "self-serve SaaS users", "target_length": 6 }
}'json
{
"data": [
{ "type": "choice", "title": "Which of the following best describes you?", "options": ["Founder", "Engineer", "PM", "Other"] },
{ "type": "text", "title": "What did you hope Krafter would help you do?" },
{ "type": "yes_no", "title": "Did you complete your first integration?" },
{ "type": "scale", "title": "How easy was setup?", "settings": { "min": 1, "max": 5 } },
{ "type": "text", "title": "What stopped you from continuing?" },
{ "type": "nps", "title": "How likely are you to recommend Krafter to a peer?" }
]
}The shape matches what POST /surveys/:id/questions accepts — pass each entry through with a position to persist it.
AI: Review Questions
POST /surveys/:id/reviewRequired scope: surveys:write
Sends every existing question on the survey to the AI engine for a quality review. Returns suggestions (clarity, leading-question warnings, ordering, redundancy) without modifying the survey.
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/review \
-H "Authorization: Bearer kr_live_abc123def456"json
{
"data": [
{
"question_id": "q1c2d3e4-...",
"issue": "leading",
"severity": "warning",
"message": "\"How great was our onboarding?\" presupposes the answer is positive. Suggest neutral phrasing.",
"suggested_title": "How would you describe your onboarding experience?"
},
{
"question_id": "q3c2d3e4-...",
"issue": "redundant",
"severity": "info",
"message": "Overlaps with question q4 (\"What could we improve?\"). Consider merging."
}
]
}The exact shape of suggestions is owned by the AI engine and may evolve. Treat fields as advisory — your dashboard / tooling should render unknown keys as plain text.
Get Summary
GET /surveys/:id/summaryRequired scope: surveys:read
Returns the latest AI-generated summary of completed responses for this survey.
bash
curl https://app.krafter.dev/api/v1/surveys/SURVEY_ID/summary \
-H "Authorization: Bearer kr_live_abc123def456"json
{
"data": {
"id": "sm1c2d3e4-...",
"summary": "Most respondents discovered Krafter via Search (50%) and Social (30%). Setup is consistently rated 8+/10, but 18% of free-text answers mention DNS verification as a sticking point.",
"insights": [
"Search remains the dominant acquisition channel.",
"DNS verification is the most-mentioned blocker.",
"NPS is healthy (avg 8.4) but skewed toward 9-10."
],
"response_tags": {
"blocker": 35,
"praise": 110,
"feature_request": 24
},
"confidence": 0.82,
"response_count": 200,
"generated_at": "2026-05-09T08:00:00Z"
}
}| Field | Type | Description |
|---|---|---|
id | string | UUID of the summary record |
summary | string | Free-text overview — typically 2-4 sentences |
insights | string[] | Bullet-point insights extracted from completed responses |
response_tags | object | {tag: count} — categorical tags applied to free-text answers |
confidence | number | Engine self-reported confidence, 0.0-1.0 |
response_count | integer | Number of responses analysed at generation time |
generated_at | string | ISO 8601 timestamp |
Returns 404 No summary available when no summary has been generated yet. Use POST /surveys/:id/summary to enqueue one.
Generate Summary
POST /surveys/:id/summaryRequired scope: surveys:write
Enqueues an Oban job (Krafter.Workers.SurveySummaryGenerator) that fetches completed responses, runs them through the AI engine, and writes a new SurveySummary row. Returns immediately — the response says the job is started, not that the summary is ready.
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/summary \
-H "Authorization: Bearer kr_live_abc123def456"json
// 202 Accepted
{ "ok": true, "message": "Summary generation started" }When generation finishes, a summary.generated webhook fires (if subscribed). Otherwise, poll GET /surveys/:id/summary — the generated_at timestamp will advance.
Cost considerations
Summaries call an LLM with the full set of completed responses. Cost scales with response count and answer length. The dashboard rate-limits regenerations; if you call this endpoint programmatically, throttle accordingly.
Errors
404 Not Found— the survey doesn't belong to your team404 No summary available—GET /summarywhen no summary has been generated502 Bad Gateway— AI engine returned an upstream error. Retry the request503 Service Unavailable— AI engine not configured. The endpoint is unavailable until the platform operator wires up an LLM provider