# Building a survey

Create a survey from a short draft, publish it, change its questions and settings, and know which limits apply along the way.

This guide creates a survey through the API, publishes it, and changes it afterwards. It needs a key with the scope
`surveys:write`, and `surveys:read` to read the survey back. The shell variables `SF_API` and `SF_KEY` are set up as
in the [Quickstart](https://simplefeedback.de/developer/docs/quickstart.md).

## How a survey is built

A survey is a list of pages, called `modules`, plus `settings`:

- a **start** page with a headline and a button
- one or more **questions** pages
- an **end** page that thanks the respondent

Every text is an object keyed by language code, for example `{ "en": "How was the workshop?" }`. The full shape has
around thirty fields per page: ids, order, presentation defaults. You rarely write it by hand. Describe the survey as
a short **draft** instead, and the server builds the rest.

## Create a survey from a draft

```bash
curl -X POST "$SF_API/v1/surveys" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Workshop feedback",
    "draft": {
      "pollName": "Workshop feedback",
      "language": "en",
      "welcomeTitle": "Thanks for joining the workshop",
      "questions": [
        { "kind": "rating", "text": "How was the workshop?" },
        { "kind": "choice", "text": "Which session did you attend?", "options": ["Morning", "Afternoon", "Evening"] },
        { "kind": "nps", "text": "How likely are you to recommend us?" },
        { "kind": "open_text", "text": "Anything we should improve?" }
      ]
    }
  }'
```

The answer is `201` with the whole survey, including the generated `modules` and `settings`. Keep its `id`:

```bash
export SURVEY_ID="4c303674-1dd4-4836-ada7-0092fe2c7df3"
```

`name` is the internal name shown in the dashboard; `draft.pollName` is the survey's own name. Without `venueId` the
survey goes to the organization's default venue, or to the key's venue for a [venue key](https://simplefeedback.de/developer/docs/authentication.md#keys-for-one-venue).

### Draft fields

| Field                | Required | Meaning                                |
| -------------------- | -------- | -------------------------------------- |
| `pollName`           | yes      | Name of the survey                     |
| `language`           | yes      | Language code all texts are written in |
| `welcomeTitle`       | yes      | Headline of the start page             |
| `welcomeDescription` | no       | Text under the headline                |
| `startButtonText`    | no       | Label of the start button              |
| `questions`          | yes      | The questions, in order                |
| `endTitle`           | no       | Headline of the end page               |
| `endMessage`         | no       | Text on the end page                   |

### Question kinds

Every question has a `kind` and a `text`, and optionally a `subtitle`.

| `kind`      | Asks for                             | Extra fields                                                                                                                                                 |
| ----------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `rating`    | A rating on a scale                  | `ratingSymbol`: `star`, `heart`, `thumbs_up`, `smiley` or `number`; `ratingLevels`: 3 to 10, default 5                                                       |
| `choice`    | One or more of a list of options     | `options`: at least two labels; `multipleChoice`: `true` to allow several; `exclusiveOptions`: labels that rule out every other answer, like "None of these" |
| `nps`       | Net Promoter Score, 0 to 10          | none                                                                                                                                                         |
| `open_text` | Free text                            | `multiline`: `false` for a single line                                                                                                                       |
| `section`   | Nothing; a heading between questions | none                                                                                                                                                         |

In a full `modules` body the same thing is `exclusive: true` on the option itself. A respondent who picks an exclusive
option loses the other ticks, and picking another option clears the exclusive one.

An invalid draft answers `400 VALIDATION_FAILED` with the message `Invalid survey draft`, and `details.errors` says
which field is wrong. Nothing is created.

## Publish it

A new survey is a draft that respondents can't open yet. Publish it with a separate call:

```bash
curl -X PATCH "$SF_API/v1/surveys/$SURVEY_ID" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "active": true }'
```

Publishing needs two things:

- The person who created the API key has verified their email address. Otherwise the call answers
  `403 EMAIL_NOT_VERIFIED`, with `details.reason` set to `EMAIL_NOT_VERIFIED` as well.
- The plan has room for another active survey. Otherwise it answers `409 TIER_LIMIT_REACHED`.

> **Note:** You can also send `"active": true` with the `POST`. If publishing then fails, the survey still exists as
> a draft and the error carries its id in `details.createdSurveyId`. Creating and publishing in two calls keeps each
> failure tied to one step.

Once published, [share its link or print QR codes](https://simplefeedback.de/developer/docs/distributing-a-survey.md).

## Change a question

`PATCH` replaces every field you send as a whole, so change a question by editing the survey you read:

1. Read the survey with `GET /v1/surveys/$SURVEY_ID`.
2. Change what you need in its `modules` array, for example a question's text.
3. Send the whole array back:

```bash
curl -X PATCH "$SF_API/v1/surveys/$SURVEY_ID" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Content-Type: application/json" \
  -d @modules.json
```

where `modules.json` is `{ "modules": [ ... ] }` with all pages. Keep the `id` of every question you don't mean to
replace: responses refer to questions by id, so a question with a new id starts without answers.

> **Warning:** A `PATCH` with a shorter `modules` array removes the pages and questions you left out.
> `POST /v1/surveys/$SURVEY_ID/undo` puts the survey back the way it was before your last change; over [MCP](https://simplefeedback.de/developer/docs/mcp.md)
> the same step is `undo_last_change`.

## Change the settings

`settings` holds the survey's name and languages (`general`), how it is shown (`display`) and how often a respondent may
answer (`participation`). Like `modules`, it is replaced as a whole and validated as a whole: read it, change a
field, and send all of it back.

```json
{
  "settings": {
    "general": { "active": false, "pollName": "Workshop feedback", "languages": ["en"], "defaultLanguage": "en" },
    "display": {
      "pageMode": "one_page",
      "theme": { "font": "Inter", "colorScheme": "system" },
      "showProgressBar": true,
      "showQuestionNumbers": true
    },
    "participation": { "repeatParticipation": "daily", "allowModuleBack": true, "allowQuestionBack": true }
  }
}
```

A `settings` object with parts missing answers `400 VALIDATION_FAILED`.

| Setting                             | Values                                                                                                     |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `display.pageMode`                  | `one_page`, `one_page_per_module`, `one_page_per_question`                                                 |
| `display.pagePerSection`            | `true`: each section starts a page with its questions; otherwise one per page                              |
| `participation.repeatParticipation` | `always`, `once`, `daily`, `weekly`, `monthly`: how often one device may answer                            |
| `participation.accessMode`          | `open` (or missing): anyone with the link; `code`: only with an unused [single-use code](https://simplefeedback.de/developer/docs/single-use-codes.md) |

`daily`, `weekly` and `monthly` count 24 hours, 7 days and 30 days from the device's last response.

## Add a header, footer and logo

`branding` sets the bar above and below every page. Each has a `mode`: `default` shows the Simple Feedback bar,
`custom` shows your text, links and logo, `hidden` shows nothing. `surface` tints the bar with `default`, `tinted`,
`accent` or `bold`, a colour role from the survey's theme. The full shape is in the
[API reference](https://simplefeedback.de/api/docs) and, for agents, in the MCP resource `simplefeedback://schema/survey-branding`.

For a logo, upload the image first. This needs the scope `files:write`:

```bash
curl -X POST "$SF_API/v1/images" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"kind\": \"survey_logo\", \"fileName\": \"logo.png\", \"imageBase64\": \"$(base64 -w0 logo.png)\"}"
```

```json
{
  "id": "3dea5e0b-2c14-42a5-85fc-386b84768671",
  "kind": "survey_logo",
  "width": 400,
  "height": 400
}
```

The image is PNG, JPEG or WebP and at most 2 MB. Then put the `id` into the header:

```bash
curl -X PATCH "$SF_API/v1/surveys/$SURVEY_ID" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "branding": {
      "header": {
        "mode": "custom",
        "logoAssetId": "3dea5e0b-2c14-42a5-85fc-386b84768671",
        "text": { "en": "Visitor survey" },
        "surface": "tinted"
      },
      "footer": {
        "mode": "custom",
        "text": { "en": "Thank you for your time." },
        "links": [{ "id": "web", "label": { "en": "Our website" }, "url": "https://example.com", "icon": "web" }],
        "showPoweredBy": true
      }
    }
  }'
```

The same id works as a start page's `logoAssetId`. Upload with `"kind": "survey_background"` for a page's
`background.backgroundImageAssetId`. Images count against your plan's storage, and an image that no survey refers to
within 24 hours is deleted.

## Delete it

```bash
curl -X DELETE "$SF_API/v1/surveys/$SURVEY_ID" -H "Authorization: Bearer $SF_KEY"
```

The answer is `204`. The survey and its link stop working at once. It stays in the trash for 30 days, with its
responses, and is then removed for good.

## Restore it

```bash
curl "$SF_API/v1/surveys/deleted" -H "Authorization: Bearer $SF_KEY"
curl -X POST "$SF_API/v1/surveys/$SURVEY_ID/restore" -H "Authorization: Bearer $SF_KEY"
```

The first call lists the trash, most recently deleted first, with `deletedAt` and `purgeAt` for each survey. The
second brings one back with its responses and returns it as `GET /v1/surveys/{id}` does, published or not as it was
when it was deleted. A survey that isn't deleted comes back unchanged, so a retry is harmless. A restored survey
counts against your plan like a new one, so a full plan answers `409 TIER_LIMIT_REACHED`; after its 30 days the
answer is `404 SURVEY_NOT_FOUND`.

## Limits

Your plan limits how many surveys can be active and how many pages and languages a survey may have. A create or
change that goes over a limit answers `409 TIER_LIMIT_REACHED` and changes nothing. Custom branding needs a plan
that includes it, otherwise `403 FEATURE_NOT_IN_TIER`. An image upload over the plan's storage answers
`403 TIER_LIMIT_REACHED`. All codes are in [Errors](https://simplefeedback.de/developer/docs/errors.md).

To check before you hit a limit, read the plan and what it already uses:

```bash
curl "$SF_API/v1/usage" \
  -H "Authorization: Bearer $SF_KEY"
```

Each limit comes with its count, for example `"surveys": { "active": 2, "limit": 5 }`. A `null` limit means
unlimited.
