BETA
Skip to content

Respondents

When you want to invite a known list of people — instead of (or in addition to) embedding a public survey — use the respondents API. Each respondent gets a unique invitation token tied to a single response, so you can attribute answers back to a specific person.

Why use respondents

Without respondents, anyone with the survey slug can submit a response. The embed widget and the public LiveView at /s/:slug are the right tool for that.

With respondents, you get:

  • Per-recipient links/s/:slug/:token automatically pre-fills respondent_token on start_response
  • Status tracking — see who has been invited (invited), opened the survey (opened), and completed it (completed)
  • Email send — Krafter sends the initial invitation email automatically
  • Response attribution — the response_id field on the respondent links them to the response they submitted

The Respondent object

FieldTypeDescription
idstringUUID
survey_idstringOwning survey
emailstringRecipient address. Unique per (survey_id, email)
tokenstringURL-safe random token (32 chars). Used in invitation links
statusstringinvited, opened, or completed
invited_atstringISO 8601, set on creation
reminded_atstring | nullISO 8601, set when reminder is recorded
response_idstring | nullUUID of the linked response, set when the respondent completes
created_atstringISO 8601 timestamp

Each respondent has a token. The invitation link served from email is:

https://app.krafter.dev/s/<survey-slug>/<token>

When the respondent opens that URL, the public survey LiveView reads the token, links the new response to the respondent record, and the respondent's status flips from invitedopenedcompleted as they progress.

If they instead visit the bare /s/<survey-slug> URL (without a token), they get a generic anonymous response — not tied to any respondent record.


Add a Respondent

POST /api/v1/surveys/:survey_id/respondents

Required scope: surveys:write

Creates one respondent and immediately enqueues an invitation email via Krafter.Workers.SurveyInvitation. The invite contains the survey name and a button linking to /s/<slug>/<token>.

Request Body

FieldTypeRequired
emailstringYes
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/respondents \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com"}'
json
// 201 Created
{
  "data": {
    "id": "r1c2d3e4-...",
    "survey_id": "b1c2d3e4-...",
    "email": "alice@example.com",
    "token": "Xk9_4vP-q...",
    "status": "invited",
    "invited_at": "2026-05-10T10:00:00Z",
    "reminded_at": null,
    "response_id": null,
    "created_at": "2026-05-10T10:00:00Z"
  }
}

Adding the same email twice for the same survey returns 422 with errors.email: ["has already been taken"].

Sender address

The invitation email is sent from surveys@noreply.krafter.dev by default — or whatever the platform operator has configured as smtp_from_email. There is no per-team customisation today.


Bulk Add Respondents

POST /api/v1/surveys/:survey_id/respondents/bulk

Required scope: surveys:write

Add up to 500 emails in a single request. The whole batch is wrapped in a database transaction — if any single email fails validation (already exists, malformed, etc.), the entire batch is rolled back.

Request Body

FieldTypeRequired
emailsstring[]Yes
bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/respondents/bulk \
  -H "Authorization: Bearer kr_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": ["alice@example.com", "bob@example.com", "carol@example.com"]
  }'
json
// 201 Created
{
  "data": [
    { "id": "r1...", "email": "alice@example.com", "status": "invited", "...": "..." },
    { "id": "r2...", "email": "bob@example.com",   "status": "invited", "...": "..." },
    { "id": "r3...", "email": "carol@example.com", "status": "invited", "...": "..." }
  ]
}

Errors

  • 422 Unprocessable Entity with error: "Too many respondents. Maximum 500 emails per request." when the array exceeds 500
  • 422 with the offending changeset when any single email fails validation — the whole batch is rolled back, so re-submit with the bad address removed

For larger lists, chunk into 500-email batches client-side.


List Respondents

GET /api/v1/surveys/:survey_id/respondents

Required scope: surveys:read

Returns every respondent for the survey, ordered by inserted_at desc. There is no server-side pagination on this endpoint today; if you have thousands of respondents, expect the response body to be large.

bash
curl https://app.krafter.dev/api/v1/surveys/SURVEY_ID/respondents \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": [
    { "id": "r1...", "email": "alice@example.com", "status": "completed",
      "response_id": "f1...", "...": "..." },
    { "id": "r2...", "email": "bob@example.com", "status": "opened",
      "response_id": null, "...": "..." }
  ]
}

Mark a Respondent as Reminded

POST /api/v1/surveys/:survey_id/respondents/:respondent_id/remind

Required scope: surveys:write

bash
curl -X POST https://app.krafter.dev/api/v1/surveys/SURVEY_ID/respondents/RESPONDENT_ID/remind \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "data": {
    "id": "r1...",
    "reminded_at": "2026-05-15T10:00:00Z",
    "...": "..."
  }
}

This endpoint does NOT send an email today

The endpoint sets reminded_at to the current timestamp — that's it. The reminder-email worker exists (Krafter.Workers.SurveyReminder) but is not enqueued by this controller in the current build.

Until that wiring lands, treat POST /remind as a bookkeeping call: useful for marking that you sent a reminder via your own ESP, or to filter the respondent list against. If you need the actual email sent, do it through your own email infrastructure (and use the survey's per-respondent link https://app.krafter.dev/s/<slug>/<token> from the List Respondents response).


Lifecycle

created
  └─> "invited"  (POST /respondents enqueues SurveyInvitation worker → email sent)
        ├─> "opened"     (LiveView records when /s/:slug/:token is loaded)
        │     └─> "completed" (response.completed flips status, sets response_id)
        └─> reminded_at set (POST /remind — bookkeeping only today)

status is read-only from the API surface — there is no PATCH /respondents/:id endpoint. Status transitions are driven by the public LiveView.


Combining respondents with the embed script

The embed script (/embed/surveys.js) doesn't know about respondents — it always loads the anonymous /s/:slug?embed=true route. If you want a per-recipient experience inside the embed:

  1. Generate the per-respondent link server-side: https://app.krafter.dev/s/<slug>/<token>
  2. Embed an iframe with that exact URL — bypass the embed script
  3. The respondent record will be linked just like the email-driven flow

In other words: the embed script is for anonymous surveys; respondent tokens are for known-recipient flows. Mixing them requires bypassing the convenience widget.


Errors

  • 404 Not Found — survey doesn't belong to your team, or respondent doesn't belong to the survey
  • 422 Unprocessable Entityemail invalid, missing, or already exists for the survey
  • 422 Unprocessable Entityemails array exceeds 500 entries

Built by Krafter Studio