# Single-use codes

Give each respondent a personal code for exactly one response, on a ticket, receipt or card, and create, print, check and revoke the codes over REST or MCP.

A survey in code mode can only be answered with a personal code, and each code allows exactly one response. You
create the codes over the API and hand one to each person: in a link, as a QR code on a ticket or receipt, or
printed on a card. Use it when every response should come from someone you invited, for example:

- **Events and trade fairs:** the ticketing system adds a code to each ticket, so every attendee rates the event
  once.
- **Shops and repair services:** the till prints a QR code with its own code on each receipt, so each purchase or
  repair can be rated once.
- **Courses and workshops:** the trainer hands out one card per participant at the end of the day.
- **Employee surveys:** HR gives each employee a code, so nobody answers twice and nobody from outside answers.
- **Hotels:** the booking system sends a link with a code at checkout, so each stay is rated once.

Reading codes needs a key with `access-codes:read`; creating and revoking them needs `access-codes:write`. Neither
scope can change a survey, so the key in a till or a ticketing system can mint codes without being able to rewrite
your surveys. Switching a survey to code mode is a survey change and needs `surveys:write`. Code mode needs a plan
that includes single-use codes; otherwise the calls answer `403 FEATURE_NOT_IN_TIER`. The shell variables
`SF_API`, `SF_KEY` and `SURVEY_ID` are set up as in the [Quickstart](https://simplefeedback.de/developer/docs/quickstart.md).

## Switch the survey to code mode

Code mode is `settings.participation.accessMode`: `code` lets only people with an unused code answer; `open`, or no
value, lets anyone with the link answer. `settings` is replaced as a whole, so read it, change the one field and send
it back:

```bash
curl -s "$SF_API/v1/surveys/$SURVEY_ID" -H "Authorization: Bearer $SF_KEY" \
  | jq '{ settings: (.settings | .participation.accessMode = "code") }' \
  | curl -X PATCH "$SF_API/v1/surveys/$SURVEY_ID" \
      -H "Authorization: Bearer $SF_KEY" \
      -H "Content-Type: application/json" \
      -d @-
```

In code mode the code is the only rule against answering twice: `participation.repeatParticipation`, which limits
each device, no longer applies, so two people sharing one phone can each answer with their own code. The start and
end date and the maximum number of responses still apply. You can create codes before or after switching.

Switching back to `open` is always allowed and lets anyone with the link answer again. After a downgrade to a plan
without single-use codes, a survey in code mode keeps asking for codes; it never opens up on its own. Its existing
codes keep working and can still be listed and revoked, but no new ones can be created.

## Create codes

```bash
curl -X POST "$SF_API/v1/surveys/$SURVEY_ID/access-codes" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Idempotency-Key: 0b8f6a52-5d1e-4f0e-9a43-7c2d8e1b9f60" \
  -H "Content-Type: application/json" \
  -d '{ "count": 200, "label": "Trade fair, day 1", "includeImage": true }'
```

The answer is `201` with one entry per code:

```json
{
  "data": [
    {
      "id": "cmvbatbky000bp57dddn3fp5i",
      "code": "8PJN4-GS8FX",
      "label": "Trade fair, day 1",
      "url": "https://simplefeedback.de/p/4c303674-1dd4-4836-ada7-0092fe2c7df3?code=8PJN4-GS8FX",
      "qrDataUrl": "data:image/png;base64,iVBORw0KGgo..."
    }
  ]
}
```

- `count` is 1 to 500 per call. A survey holds at most 10 000 codes; a call that would go over answers
  `409 ACCESS_CODE_LIMIT_REACHED` and creates none.
- `label` is an optional note for the batch, up to 255 characters, shown when you list the codes. Don't put names of
  people in it; see [Privacy](#privacy).
- `includeImage: true` adds `qrDataUrl` to every code: a 512 × 512 PNG of its `url` for printing, about 5 kB each.
- A code is ten characters, printed as `XXXXX-XXXXX`, from digits and capital letters without I, L, O and U.

> **Warning:** The codes are in this answer only. Simple Feedback stores just a hash of each code, so they cannot be
> read again later. Save them, print them or hand them on before you do anything else; a lost code can only be
> revoked and replaced.

Send an [`Idempotency-Key`](https://simplefeedback.de/developer/docs/overview.md#conventions) so that a retry after a timeout returns the same codes instead of
creating a second batch. A till that creates one code per receipt can use the receipt number as the key.

## Hand them out

Every code reaches its respondent inside `url`, the survey link with the code in it. Opening it shows the survey
with the code filled in; the code is checked when the survey opens and again when the response is sent.

| Where           | How                                                                                |
| --------------- | ---------------------------------------------------------------------------------- |
| Email, app, SMS | Put `url` behind a button or link                                                  |
| Ticket, receipt | Print the code's QR code: `qrDataUrl`, or a QR code of `url` from your own library |
| Cards, flyers   | Print one QR code per card                                                         |

These QR codes are not [saved QR codes](https://simplefeedback.de/developer/docs/distributing-a-survey.md#link-or-saved-qr-code): they don't count against your
plan's QR code limit, don't appear in the dashboard's QR code list and their scans are not counted in QR code
statistics.

To print a code again, send it back and get its link and QR code:

```bash
curl -X POST "$SF_API/v1/surveys/$SURVEY_ID/access-codes/qr" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "8PJN4-GS8FX" }'
```

The answer is `{ "url": "…", "qrDataUrl": "data:image/png;base64,…" }`. It changes nothing and is a `POST` only so
the code stays out of URLs and access logs. Case, spaces and hyphens don't matter, and O, I and L are read as 0, 1
and 1. A code that is not one of this survey's answers `404 ACCESS_CODE_NOT_FOUND`.

## What respondents see

A respondent who opens the survey without a code, with an unknown or revoked code, or with a code that was already
used is told so and can't answer. The same reasons come back from the public survey routes the survey page uses:

| Code                   | Status | Meaning                                     |
| ---------------------- | ------ | ------------------------------------------- |
| `ACCESS_CODE_REQUIRED` | 403    | The survey is in code mode and no code came |
| `ACCESS_CODE_INVALID`  | 403    | The code is unknown or revoked              |
| `ACCESS_CODE_USED`     | 409    | The code was already used for a response    |

Unknown and revoked codes give the same answer, so trying codes reveals nothing about them. A code is used only by
a response that is accepted: a response refused for another reason, for example because the survey has ended,
leaves the code unused. When two responses arrive with the same code at the same moment, exactly one is accepted.

## Check which codes were used

```bash
curl "$SF_API/v1/surveys/$SURVEY_ID/access-codes?status=used" \
  -H "Authorization: Bearer $SF_KEY"
```

```json
{
  "data": [
    {
      "id": "cmvbatbky000bp57dddn3fp5i",
      "label": "Trade fair, day 1",
      "status": "used",
      "createdAt": "2026-10-16T18:25:54.658Z",
      "usedOn": "2026-10-16",
      "revokedAt": null
    }
  ],
  "pagination": { "total": 1, "limit": 25, "offset": 0 }
}
```

`status` is `unused`, `used` or `revoked`; filter by it with `status`, page with `limit` and `offset` as on every
list. The codes themselves are never returned. `usedOn` is only the day, with no time. To count codes, ask for one
status with `limit=1` and read `pagination.total`.

## Revoke the rest

When the event is over or a batch was printed by mistake, revoke the codes that are still unused:

```bash
curl -X POST "$SF_API/v1/surveys/$SURVEY_ID/access-codes/revoke" \
  -H "Authorization: Bearer $SF_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "all": true }'
```

Send `{ "all": true }` for every unused code, or `{ "ids": ["…"] }` with up to 500 ids from the list; one of the two,
or the call answers `400 VALIDATION_FAILED`. The answer is `{ "revoked": 12 }`, the number of codes revoked. Only
unused codes are revoked; a used code stays used and its response stays. There is no undo.

## Read the responses

Responses of a survey in code mode are read like any other, with `GET /v1/surveys/{id}/responses` and
`/statistics`; see [Syncing results](https://simplefeedback.de/developer/docs/syncing-results.md). A response carries nothing about the code it was sent with.

## Privacy

Simple Feedback stores no link between a response and its code: the response doesn't name the code, the code doesn't
name the response, and `usedOn` is a day, not a time. Whether responses stay anonymous depends on how you hand the
codes out:

- **Codes handed out without names** (a stack of cards at the door, a code on every receipt) keep responses
  anonymous. Nobody can tell who had which code.
- **Codes given to named people** (a list of who got which code, a label with a name, a link in a personal email
  that you keep) make responses pseudonymous at best. With few responses per day, the day a code was used can be
  enough to tell whose response is whose.

In the second case you process personal data: you need a legal basis for it and must tell respondents before they
answer. Keep `label` for batches ("Trade fair, day 1", "Course B"), never for people. Deleting a survey deletes its
codes with it.

## Over MCP

An [AI assistant](https://simplefeedback.de/developer/docs/mcp.md) does the same with four tools. Code mode is switched on with `update_survey`, which sends
the whole `settings` like the `PATCH` above.

| Tool                  | Scope                | Over REST                                   |
| --------------------- | -------------------- | ------------------------------------------- |
| `create_access_codes` | `access-codes:write` | `POST /v1/surveys/{id}/access-codes`        |
| `list_access_codes`   | `access-codes:read`  | `GET /v1/surveys/{id}/access-codes`         |
| `revoke_access_codes` | `access-codes:write` | `POST /v1/surveys/{id}/access-codes/revoke` |
| `get_access_code_qr`  | `access-codes:read`  | `POST /v1/surveys/{id}/access-codes/qr`     |

`create_access_codes` takes `count` and `label` but no `includeImage`: the codes and their links come back as text,
and the tool tells the assistant to hand them to you at once, since they can't be read again. For a printable image
of one code, the assistant calls `get_access_code_qr` with that code. `revoke_access_codes` is marked as
destructive, so clients such as Claude ask before calling it. All four take an optional `idempotencyKey`.
