BETA
Skip to content

Responses API

Read, export, and delete survey responses (authenticated), and submit responses from a respondent's browser (public, slug-based, no API key).

Base URL: https://app.krafter.dev/api/v1

The response API has two distinct surfaces:

  • Authenticated — your team reading and managing collected responses (GET /surveys/:id/responses, etc.). Requires surveys:read / surveys:write.
  • Public — the respondent submitting answers from a browser or your own form (POST /surveys/:slug/responses, etc.). Slug-based, no auth, CORS-enabled, stricter rate limit.

The Response object

FieldTypeDescription
idstringUUID
survey_idstringUUID of the survey
respondent_tokenstring | nullPer-respondent token for invitation flows. Set on start_response when the respondent opens an invite link
respondent_emailstring | nullEmail address if known (invitation flow or email-typed answer copied through)
answersobject{question_id: answer_value} — see Answer shapes
question_versionsobject{question_id: version} snapshot of question versions at submit time. Lets the dashboard render historical responses against the question shape they saw
ai_conversationobject[]AI-mode only. Ordered transcript of {role, question, answer} turns. Empty for manual surveys
ai_cost_centsintegerToken cost in cents. 0 for manual surveys
metadataobjectFree-form. Embed widget writes {ip, user_agent, referrer, country}; you can pass your own keys via start_response and save_progress
statusstringin_progress, completed, or abandoned
started_atstringISO 8601, set on start_response
completed_atstring | nullISO 8601, set on complete_response
created_atstringISO 8601, same as started_at for normal flow

Authenticated endpoints

List Responses

GET /surveys/:survey_id/responses

Required scope: surveys:read

Query parameters

ParamTypeDescription
pageinteger1-based page. Default 1
per_pageintegerItems per page. Default 20, max 100
statusstringFilter by in_progress, completed, or abandoned

Example Request

bash
curl "https://app.krafter.dev/api/v1/surveys/SURVEY_ID/responses?status=completed" \
  -H "Authorization: Bearer kr_live_abc123def456"

Example Response

json
{
  "data": [
    {
      "id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
      "survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
      "respondent_token": null,
      "respondent_email": "alice@example.com",
      "answers": {
        "q1c2d3e4-...": "A landing page form",
        "q2c2d3e4-...": "Search",
        "q3c2d3e4-...": 9
      },
      "question_versions": {
        "q1c2d3e4-...": 1,
        "q2c2d3e4-...": 1,
        "q3c2d3e4-...": 1
      },
      "ai_conversation": [],
      "ai_cost_cents": 0,
      "metadata": {
        "ip": "203.0.113.42",
        "user_agent": "Mozilla/5.0 ...",
        "country": "US"
      },
      "status": "completed",
      "started_at": "2026-05-10T10:05:00Z",
      "completed_at": "2026-05-10T10:06:30Z",
      "created_at": "2026-05-10T10:05:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 20, "total": 1, "total_pages": 1 }
}

Get Response

GET /surveys/:survey_id/responses/:response_id

Required scope: surveys:read

Returns a single response. Returns 404 if the response belongs to a different survey or team — even if you know the id.


Delete Response

DELETE /surveys/:survey_id/responses/:response_id

Required scope: surveys:write

Permanently deletes one response. The survey's response_count is decremented if the response was completed.

204 No Content

Bulk delete

There is no bulk-delete endpoint. To wipe all responses for a survey, fetch ids via List Responses and delete them one by one, or delete and recreate the survey.


Export Responses

POST /surveys/:survey_id/responses/export

Required scope: surveys:read

Streams every response for a survey. Two formats:

formatOutput
"csv" (recommended)text/csv chunked stream, one row per response. Headers: id, respondent_email, respondent_token, status, started_at, completed_at, created_at, answer:<question_id>, .... List answers are joined with ;; map answers are JSON-encoded; cells starting with =/+/-/@/tab/CR are prefixed with ' to defuse spreadsheet formula injection
"json" (default)First 10,000 responses returned in a single JSON array. No streaming — use CSV for larger surveys

Example Request

bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/responses/export \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{"format": "csv"}' \
  -o responses.csv

The CSV is downloaded with Content-Disposition: attachment; filename="<slug>-responses.csv".


Public endpoints

The public response flow is slug-based, unauthenticated, and CORS-enabled — designed to be called directly from a respondent's browser (or from the embed widget). Three calls form a single response:

  1. POST /api/v1/surveys/:slug/responses — start a response
  2. PATCH /api/v1/surveys/:slug/responses/:response_id — save answers (any number of times)
  3. POST /api/v1/surveys/:slug/responses/:response_id/complete — finalise

Rate limit (public)

The public response endpoints share the public rate-limit bucket:

  • start_response, complete_response, ai_next_questionstricter per-IP bucket (write/AI traffic)
  • save_progress — standard public bucket

See Rate limits for current numbers. Survey must be active — calls against draft or closed surveys return 404 Not Found (the slug is not exposed).

Start Response

POST /api/v1/surveys/:slug/responses

Auth: none.

Request Body

FieldTypeRequiredDescription
respondent_tokenstringNoSet this when the respondent opened a per-recipient invitation link. Lets the response be linked back to the respondent record
respondent_emailstringNoEmail if known up-front
metadataobjectNoCustom metadata (UTM, referrer, anything you want preserved on the response). Merged with widget-provided fields
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/product-feedback/responses \
  -H "Content-Type: application/json" \
  -d '{}'
json
// 201 Created
{
  "data": {
    "id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
    "survey_id": "b1c2d3e4-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
    "respondent_token": null,
    "respondent_email": null,
    "answers": {},
    "question_versions": {},
    "ai_conversation": [],
    "ai_cost_cents": 0,
    "metadata": {},
    "status": "in_progress",
    "started_at": "2026-05-10T10:05:00Z",
    "completed_at": null,
    "created_at": "2026-05-10T10:05:00Z"
  }
}

The returned id is the only handle you need for save-progress and complete. Treat it as opaque — it is not signed, but a respondent who loses it cannot resume their response.


Save Progress

PATCH /api/v1/surveys/:slug/responses/:response_id

Auth: none.

Allows the embed widget (or your own front-end) to persist partial progress as the respondent answers. Multiple PATCHes are allowed — each call overwrites answers, question_versions, ai_conversation, and metadata with whatever you send. Send the full state each time.

Request Body

FieldTypeDescription
answersobject{question_id: value} map
question_versionsobject{question_id: version} — copy version from each question at the moment the respondent saw it
ai_conversationobject[]AI-mode only. Append-only transcript
metadataobjectFree-form
bash
curl -X PATCH https://app.krafter.dev/api/v1/surveys/product-feedback/responses/$RESPONSE_ID \
  -H "Content-Type: application/json" \
  -d '{
    "answers": {
      "q1c2d3e4-...": "A landing page form",
      "q2c2d3e4-...": "Search"
    },
    "question_versions": {
      "q1c2d3e4-...": 1,
      "q2c2d3e4-...": 1
    }
  }'

Save-progress does not change status — the response stays in_progress until you call complete.


Complete Response

POST /api/v1/surveys/:slug/responses/:response_id/complete

Auth: none.

Finalises the response. Sets status: "completed", sets completed_at, increments the survey's response_count, and triggers any response.completed webhooks.

bash
curl -X POST https://app.krafter.dev/api/v1/surveys/product-feedback/responses/$RESPONSE_ID/complete \
  -H "Content-Type: application/json"
json
{
  "data": {
    "id": "f1a2b3c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
    "status": "completed",
    "completed_at": "2026-05-10T10:06:30Z",
    "...": "..."
  }
}

A second complete against the same response is a no-op — it returns the response unchanged.


AI Next Question

POST /api/v1/surveys/:slug/next

Auth: none.

For surveys with mode: "ai". Asks the AI engine to generate the next question based on the running conversation. The embed widget calls this between save-progress calls; you can call it directly if you're rendering the survey yourself.

Request Body

FieldTypeDescription
conversationobject[]The running [{role, question, answer}, ...] transcript
contextobjectOptional context the AI should consider (target persona, locale, etc.)
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/product-feedback/next \
  -H "Content-Type: application/json" \
  -d '{
    "conversation": [
      {"role": "ai", "question": "What did you build with Krafter?"},
      {"role": "user", "answer": "A signup form"}
    ],
    "context": {"locale": "en-US"}
  }'
json
{
  "data": {
    "question": "How long did the form take to set up?",
    "type": "text",
    "done": false
  }
}

When the AI engine decides the conversation is complete, the response carries "done": true and the front-end should call /complete.

Errors

  • 503 Service Unavailable — AI engine not configured (no API key, model unreachable). The embed widget falls back to ending the conversation.
  • 502 Bad Gateway — the upstream model returned an error. Retry with the same conversation.

CORS preflight

OPTIONS /api/v1/surveys/*

Returns 204 No Content with permissive CORS headers so browsers can POST/PATCH from any origin. No body, no auth.


Errors

  • 404 Not Found — survey slug doesn't exist, isn't active, or the response belongs to a different survey
  • 422 Unprocessable Entityanswers keys don't match a question id, or values fail per-type validation
  • 429 Too Many Requests — rate-limit bucket exhausted. Retry after Retry-After header
  • 503 / 502 — AI engine errors (only on /next)

Built by Krafter Studio