BETA
Skip to content

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

EndpointPurposeScope
GET /surveys/:id/analyticsOverall stats + per-question breakdownssurveys:read
GET /surveys/:id/analytics/funnelDrop-off by sectionsurveys:read
POST /surveys/:id/generateAI: generate draft questions for a goalsurveys:write
POST /surveys/:id/reviewAI: review existing questionssurveys:write
GET /surveys/:id/summaryLatest AI summarysurveys:read
POST /surveys/:id/summaryEnqueue a fresh AI summarysurveys: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/analytics

Required 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

FieldTypeDescription
total_responsesintegerAll responses regardless of status
completedintegerStatus completed
in_progressintegerStatus in_progress
abandonedintegerStatus abandoned (set by the abandonment cron)
completion_ratenumbercompleted / total * 100, rounded to one decimal
avg_duration_secondsnumber | nullAvg 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 typeBreakdown 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/funnel

Required 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 }
  ]
}
FieldTypeDescription
section_idstringSection UUID
titlestring | nullSection title (may be null for unnamed sections)
startedintegerResponses with at least one answer in this section
totalintegerAll 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/generate

Required 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

FieldTypeDescription
goalstringWhat the survey is for. Defaults to the survey's name if omitted
contextobjectOptional. 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/review

Required 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/summary

Required 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"
  }
}
FieldTypeDescription
idstringUUID of the summary record
summarystringFree-text overview — typically 2-4 sentences
insightsstring[]Bullet-point insights extracted from completed responses
response_tagsobject{tag: count} — categorical tags applied to free-text answers
confidencenumberEngine self-reported confidence, 0.0-1.0
response_countintegerNumber of responses analysed at generation time
generated_atstringISO 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/summary

Required 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 team
  • 404 No summary availableGET /summary when no summary has been generated
  • 502 Bad Gateway — AI engine returned an upstream error. Retry the request
  • 503 Service Unavailable — AI engine not configured. The endpoint is unavailable until the platform operator wires up an LLM provider

Built by Krafter Studio