openapi: 3.1.0
info:
  title: StrawPoll Forms API
  version: 4.0.0
  summary: Create forms and polls, collect responses and read results.
  description: |
    The Forms API (v4) manages forms on StrawPoll: polls, meeting polls, rankings, picture
    choices, surveys and quizzes built from sections.

    **Authentication.** Create a key in Settings → API keys and send it in the `X-API-Key`
    header (or as `Authorization: Bearer <key>`). A key acts as its owner. Endpoints that
    respondents use (`submit`, `verify`) work without a key.

    **Errors** are JSON: `{ "error": "Human readable message", "code": "machine_code" }`.
    `code` is present where a client might want to react to it.

    **Rate limits.** 120 requests per minute per key (per IP address without a key).
    Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
    `X-RateLimit-Reset` (Unix seconds); over the limit you get `429` with `Retry-After`.

    **Drafts.** A form has a live version that respondents see and an optional draft.
    `PUT /forms/{slug}` changes the live version directly; `PUT /forms/{slug}/draft` plus
    `POST /forms/{slug}/publish` is the editor's save-then-publish flow.

    **AI assistants.** For an AI assistant or agent, the MCP server at
    https://strawpoll.com/mcp is simpler than this API: its `create_poll` and `create_form`
    tools take a title and plain lists of options or questions. It signs in with OAuth or an
    API key. A summary for language models is at https://strawpoll.com/llms.txt.

    **Building a form.** A form is a list of `sections`; a question section holds one field,
    and choice fields hold `choices`. The section `title` is the question people see and must
    be set before publishing. Start from the examples on `createForm`.

    The previous Poll API (v3) keeps working; see https://strawpoll.com/docs/api/v3.
  contact:
    name: StrawPoll support
    email: support@strawpoll.com
  license:
    name: CC BY-SA 4.0
    url: https://creativecommons.org/licenses/by-sa/4.0/
servers:
  - url: https://api.strawpoll.com/v4
  - url: https://strawpoll.com/api/v4
security:
  - ApiKeyAuth: []
tags:
  - name: Account
  - name: Forms
  - name: Publishing
  - name: Responses
  - name: Voting
    description: Used by respondents; no API key needed.
  - name: Codes and invites
  - name: Media
  - name: Themes
paths:
  /me:
    get:
      tags: [Account]
      operationId: getMe
      summary: The account the key belongs to
      responses:
        '200':
          description: The account and its workspaces.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Me' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /forms:
    get:
      tags: [Forms]
      operationId: listForms
      summary: List forms
      description: Forms you created, newest first, or every form of a workspace you belong to.
      parameters:
        - name: workspaceId
          in: query
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Form summaries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FormSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Forms]
      operationId: createForm
      summary: Create a form
      description: |
        Creates a form. It starts as a `draft` unless you send `status: "published"`.
        Ids of sections, fields and choices are optional; send your own UUIDs to refer to
        them in the same request, otherwise they are generated.
      security:
        - ApiKeyAuth: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FormInput' }
            examples:
              poll:
                summary: A single-choice poll, published right away
                value:
                  title: Where should we go for lunch?
                  status: published
                  properties:
                    responseDuplicateCheck: ip
                    resultsVisibility: public
                  sections:
                    - type: multiple_choice
                      title: Where should we go for lunch?
                      fields:
                        - type: multiple_choice
                          label: Where should we go for lunch?
                          properties: { isMultipleChoice: false }
                          choices:
                            - label: Pizza
                            - label: Sushi
                            - label: Tacos
              survey:
                summary: A short feedback survey, saved as a draft
                value:
                  title: How was the workshop?
                  properties:
                    resultsVisibility: private
                  sections:
                    - type: content
                      title: How would you rate the workshop?
                      properties: { variant: rating }
                      fields:
                        - type: rating
                          properties: { max: 5 }
                    - type: multiple_choice
                      title: Which sessions did you attend?
                      fields:
                        - type: multiple_choice
                          properties: { isMultipleChoice: true }
                          choices:
                            - label: Keynote
                            - label: Hands-on lab
                            - label: Panel
                    - type: content
                      title: What should we do differently?
                      properties: { variant: long_text }
                      fields:
                        - type: text_large
                          properties: { required: false }
      responses:
        '200':
          description: The created form.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Form' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /forms/{slug}:
    parameters:
      - $ref: '#/components/parameters/Slug'
    get:
      tags: [Forms]
      operationId: getForm
      summary: Get a form
      description: The live form with every section, field, choice and its tallies.
      responses:
        '200':
          description: The form.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Form' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Forms]
      operationId: replaceForm
      summary: Replace a form
      description: |
        Replaces the live form. Send the whole form: sections, fields and choices that are
        left out are deleted, together with their answers. Keep the `id`s of the parts you
        keep, so responses stay attached to them.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FormInput' }
      responses:
        '200':
          description: The updated form.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Form' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Forms]
      operationId: updateForm
      summary: Rename, open/close or change settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: Shown only to you, e.g. in the dashboard.
                isOpen:
                  type: boolean
                  description: Close or reopen the form. Reopening drops a close date that already passed.
                properties: { $ref: '#/components/schemas/FormProperties' }
      responses:
        '200':
          description: The updated form.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Form' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Forms]
      operationId: deleteForm
      summary: Delete a form
      description: Deletes the form and all of its responses for good.
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /forms/{slug}/draft:
    parameters:
      - $ref: '#/components/parameters/Slug'
    put:
      tags: [Publishing]
      operationId: saveDraft
      summary: Save a draft
      description: Saves changes respondents do not see until you publish.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                themeId: { type: [string, 'null'], format: uuid }
                properties: { $ref: '#/components/schemas/FormProperties' }
                sections:
                  type: array
                  items: { $ref: '#/components/schemas/SectionInput' }
      responses:
        '200':
          description: The saved draft.
          content:
            application/json:
              schema:
                type: object
                properties:
                  title: { type: string }
                  themeId: { type: [string, 'null'] }
                  properties: { $ref: '#/components/schemas/FormProperties' }
                  sections:
                    type: array
                    items: { $ref: '#/components/schemas/SectionInput' }
    delete:
      tags: [Publishing]
      operationId: discardDraft
      summary: Discard the draft
      responses:
        '200':
          description: The live form.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Form' }

  /forms/{slug}/publish:
    parameters:
      - $ref: '#/components/parameters/Slug'
    post:
      tags: [Publishing]
      operationId: publishForm
      summary: Publish
      description: |
        Makes the draft live and sets the status to `published`. A body with `sections`
        is saved as the draft first, so you can save and publish in one call.
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FormInput' }
      responses:
        '200':
          description: The published form.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Form' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /forms/{slug}/responses:
    parameters:
      - $ref: '#/components/parameters/Slug'
    get:
      tags: [Responses]
      operationId: listResponses
      summary: List responses
      description: Every response, newest first, including invalidated ones (marked as such).
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: A page of responses.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Response' }
                  pagination:
                    type: object
                    properties:
                      limit: { type: integer }
                      offset: { type: integer }
                      total: { type: integer }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Responses]
      operationId: resetResponses
      summary: Reset the form
      description: Deletes every response and zeroes all tallies. Invitees can respond again.
      responses:
        '200': { $ref: '#/components/responses/Success' }

  /forms/{slug}/submit:
    parameters:
      - $ref: '#/components/parameters/Slug'
    post:
      tags: [Voting]
      operationId: submitResponse
      summary: Submit a response
      description: |
        Sends a response with the form's rules applied: schedule, limits, one response per person,
        captcha, VPN blocking, codes and email verification. The browser gets an `sp_voter`
        cookie, which identifies it for responses limited by browser session and later edits.
      security:
        - {}
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubmitInput' }
      responses:
        '200': { $ref: '#/components/responses/Submitted' }
        '400':
          description: The answers do not satisfy the form. `step` names the question.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    properties:
                      step: { type: string }
        '403':
          description: |
            Not accepted. `code` is one of `upcoming`, `closed`, `full`, `unavailable`,
            `decided`, `captcha`, `vpn`, `code`, `verify_email`, `disposable`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '409':
          description: '`duplicate`: this voter already responded, or the code was used.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503':
          description: '`busy`: many votes are arriving; retry after `Retry-After` seconds.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    put:
      tags: [Voting]
      operationId: editResponse
      summary: Change your response
      description: Changes a response sent earlier from the same browser, when the form allows it.
      security:
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/SubmitInput'
                - type: object
                  required: [id]
                  properties:
                    id: { type: string, format: uuid }
      responses:
        '200': { $ref: '#/components/responses/Submitted' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /forms/{slug}/verify:
    parameters:
      - $ref: '#/components/parameters/Slug'
    post:
      tags: [Voting]
      operationId: verifyEmail
      summary: Verify a voter's email
      description: |
        For forms limited to one response per email address. `{ "action": "send", "email" }`
        emails a 6-digit code; `{ "action": "verify", "email", "code" }` confirms it for this
        browser. Then submit as usual.
      security:
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action, email]
              properties:
                action: { type: string, enum: [send, verify] }
                email: { type: string, format: email }
                code: { type: string, pattern: '^\d{6}$' }
      responses:
        '200':
          description: '`{ "sent": true, "resendIn": 60 }` or `{ "verified": true }`.'
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/BadRequest' }
    delete:
      tags: [Voting]
      operationId: forgetEmail
      summary: Forget the verified email in this browser
      security:
        - {}
      responses:
        '200': { $ref: '#/components/responses/Success' }

  /forms/{slug}/codes:
    parameters:
      - $ref: '#/components/parameters/Slug'
    get:
      tags: [Codes and invites]
      operationId: listCodes
      summary: List unique codes
      responses:
        '200':
          description: Codes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Invite' } }
    post:
      tags: [Codes and invites]
      operationId: createCodes
      summary: Generate unique codes
      description: 'For forms with `responseDuplicateCheck: code`. Each code allows one response.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [count, length]
              properties:
                count: { type: integer, minimum: 1, maximum: 1000 }
                length: { type: integer, minimum: 4, maximum: 32 }
                style: { type: string, enum: [alphanumeric, numeric, letters], default: alphanumeric }
                prefix: { type: string, maxLength: 20 }
      responses:
        '201':
          description: The new codes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Invite' } }
    delete:
      tags: [Codes and invites]
      operationId: deleteCodes
      summary: Delete codes
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Ids' }
      responses:
        '200': { $ref: '#/components/responses/Success' }

  /forms/{slug}/invites:
    parameters:
      - $ref: '#/components/parameters/Slug'
    get:
      tags: [Codes and invites]
      operationId: listInvites
      summary: List email invites
      responses:
        '200':
          description: Invites.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Invite' } }
    post:
      tags: [Codes and invites]
      operationId: createInvites
      summary: Add email invites
      description: 'One invite `{ name, email, code }` or up to 1000 as `{ "invites": [...] }`. Addresses already invited are skipped.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/InviteInput'
                - type: object
                  required: [invites]
                  properties:
                    invites:
                      type: array
                      maxItems: 1000
                      items: { $ref: '#/components/schemas/InviteInput' }
      responses:
        '201':
          description: Created invites.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Invite' } }
                  skipped: { type: integer }
    delete:
      tags: [Codes and invites]
      operationId: deleteInvites
      summary: Delete invites
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Ids' }
      responses:
        '200': { $ref: '#/components/responses/Success' }

  /forms/{slug}/invites/{id}:
    parameters:
      - $ref: '#/components/parameters/Slug'
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Codes and invites]
      operationId: updateInvite
      summary: Change an invite
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InviteInput' }
      responses:
        '200':
          description: The invite.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invite' }
    delete:
      tags: [Codes and invites]
      operationId: deleteInvite
      summary: Delete an invite
      responses:
        '200': { $ref: '#/components/responses/Success' }

  /forms/{slug}/invites/import:
    parameters:
      - $ref: '#/components/parameters/Slug'
    post:
      tags: [Codes and invites]
      operationId: importInviteList
      summary: Invite everyone on one of your invite lists
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [inviteListId]
              properties:
                inviteListId: { type: string, format: uuid }
      responses:
        '201':
          description: '`{ "data": { "emails": 0, "codes": 0, "skipped": 0 } }`'

  /forms/{slug}/invites/send:
    parameters:
      - $ref: '#/components/parameters/Slug'
    post:
      tags: [Codes and invites]
      operationId: sendInvites
      summary: Email invites
      description: |
        Emails invites that were not sent yet, or the ones in `ids` (with `resend: true`
        also those sent before). `subject` and `message` may use `{{name}}`, `{{form}}`
        and `{{link}}`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject, message]
              properties:
                subject: { type: string }
                message: { type: string }
                ids: { type: array, items: { type: string, format: uuid } }
                resend: { type: boolean }
      responses:
        '200':
          description: '`{ "data": { "sent": 12 } }`'
        '503':
          description: Email delivery is not configured.

  /media:
    post:
      tags: [Media]
      operationId: uploadImage
      summary: Upload an image
      description: JPEG, PNG, GIF or WebP up to 5 MB. Use the id as `mediaId` on picture choices.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        '200':
          description: The image.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  url: { type: string, example: /media/0193e0a2-7b1c-7cc1-9a51-3f0d2c1f8e10 }
        '413': { $ref: '#/components/responses/BadRequest' }
        '415': { $ref: '#/components/responses/BadRequest' }

  /themes:
    post:
      tags: [Themes]
      operationId: createTheme
      summary: Create a theme
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                properties: { $ref: '#/components/schemas/ThemeProperties' }
      responses:
        '200':
          description: The theme.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Theme' }

  /themes/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    put:
      tags: [Themes]
      operationId: updateTheme
      summary: Change a theme
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                properties: { $ref: '#/components/schemas/ThemeProperties' }
      responses:
        '200':
          description: The theme.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Theme' }
    delete:
      tags: [Themes]
      operationId: deleteTheme
      summary: Delete a theme
      responses:
        '200': { $ref: '#/components/responses/Success' }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

  parameters:
    Slug:
      name: slug
      in: path
      required: true
      description: The form's public id, as in `https://strawpoll.com/r/{slug}`.
      schema: { type: string, maxLength: 20, example: AZPg4n1Td3-hnVAj6Q8p }

  responses:
    Success:
      description: Done.
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean, const: true }
    Submitted:
      description: |
        The saved response's id, and the responses the voter may now see (only when the
        form's results are visible to them).
      content:
        application/json:
          schema:
            type: object
            properties:
              id: { type: string, format: uuid }
              participants:
                type: array
                items: { $ref: '#/components/schemas/Participant' }
    BadRequest:
      description: The request is invalid.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: No or an invalid API key (`invalid_api_key`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: The key's owner cannot manage this form.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, example: Form not found }
        code: { type: string, example: not_found }

    Ids:
      type: object
      required: [ids]
      properties:
        ids: { type: array, items: { type: string, format: uuid } }

    Me:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        email: { type: string, format: email }
        createdAt: { type: string, format: date-time }
        workspaces:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              name: { type: [string, 'null'] }
              role: { type: string, enum: [owner, admin, member] }
              createdAt: { type: string, format: date-time }

    FormStatus:
      type: string
      enum: [draft, published, archived, deleted, blocked]
      description: '`deleted` and `blocked` are set by moderation; such forms cannot be changed.'

    FormSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        slug: { type: string }
        name: { type: string }
        status: { $ref: '#/components/schemas/FormStatus' }
        isOpen: { type: boolean }
        responseCount: { type: integer }
        workspaceId: { type: [string, 'null'], format: uuid }
        opensAt: { type: [string, 'null'], format: date-time }
        closesAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    FormInput:
      type: object
      required: [sections]
      properties:
        name: { type: string, maxLength: 255, description: Only shown to you. }
        title: { type: string, maxLength: 255, description: The heading respondents see. }
        status: { type: string, enum: [draft, published, archived] }
        workspaceId:
          type: string
          format: uuid
          description: On create; defaults to your first workspace.
        themeId: { type: [string, 'null'], format: uuid }
        properties: { $ref: '#/components/schemas/FormProperties' }
        sections:
          type: array
          items: { $ref: '#/components/schemas/SectionInput' }

    Form:
      type: object
      properties:
        id: { type: string, format: uuid }
        slug: { type: string }
        userId: { type: [string, 'null'], format: uuid }
        workspaceId: { type: [string, 'null'], format: uuid }
        name: { type: [string, 'null'] }
        title: { type: string }
        status: { $ref: '#/components/schemas/FormStatus' }
        responseCount: { type: integer }
        isOpen: { type: boolean }
        themeId: { type: [string, 'null'], format: uuid }
        theme: { oneOf: [{ $ref: '#/components/schemas/Theme' }, { type: 'null' }] }
        version: { type: string, format: uuid, description: Changes on every publish or replace. }
        properties: { $ref: '#/components/schemas/FormProperties' }
        sections:
          type: array
          items: { $ref: '#/components/schemas/Section' }
        urls:
          type: object
          properties:
            formUrl: { type: string, format: uri }
            resultsUrl: { type: string, format: uri }
            adminUrl: { type: string, format: uri }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: [string, 'null'], format: date-time }
        deletedAt: { type: [string, 'null'], format: date-time }

    FormProperties:
      type: object
      description: Form-wide settings. Unknown keys are dropped, unknown values fall back to the default.
      properties:
        language: { type: string, enum: [en, de, fr, es, it, pt], default: en }
        layout: { type: string, enum: [paging, scrolling], default: paging }
        resultsVisibility:
          type: string
          enum: [public, public_after_response, public_after_close, private]
          default: public
          description: Who sees the results. `private` means only you.
        responsesVisibility:
          type: string
          enum: [public, private]
          default: private
          description: Whether respondent names are listed next to visible results.
        showPercentagesOnly: { type: boolean, default: false }
        allowComments: { type: boolean, default: false }
        commentsVisibility: { type: string, enum: [public, private], default: private }
        blockVPNUsers: { type: boolean, default: true }
        editResponsePermissions:
          type: string
          enum: [admin_own_responses, own_responses, admin, nobody]
          default: admin_own_responses
        responseDuplicateCheck:
          type: string
          enum: [none, session, ip, name, email, code]
          default: ip
          description: |
            How repeat responses are stopped. `session` uses a browser cookie, `email` asks
            for a verified address (see /verify), `code` needs a unique code or invite.
        responseDuplicateCheckInterval:
          type: [integer, 'null']
          description: Minutes after which the same voter may respond again (60, 1440, 10080, 43200). null means never.
        useCaptchaVerification: { type: boolean, default: false }
        opensAt: { type: [string, 'null'], format: date-time }
        closesAt: { type: [string, 'null'], format: date-time }
        responseLimit: { type: [integer, 'null'], minimum: 1, maximum: 1000000 }
        sendSelfEmailNotification: { type: boolean, default: false }
        sendRespondentEmailNotification: { type: boolean, default: false }
        showProgressBar: { type: boolean, default: true }
        autoJumpToNextPage: { type: boolean, default: true }

    SectionType:
      type: string
      enum:
        - multiple_choice
        - picture_choice
        - ranking
        - meeting
        - contact_info
        - content
        - page_break
        - welcome_screen
        - thank_you_screen
        - end_screen
        - redirect
      description: |
        - `multiple_choice`, `picture_choice`, `ranking`: one choice question each (field type
          `multiple_choice`, `multiple_choice` with `mediaId` on choices, `ranking`).
        - `meeting`: one field per date (`type: date`, label `YYYY-MM-DD`) with one choice per
          option (label `HH:MM-HH:MM`, or `All day`), in the section's `timezone`.
        - `content`: text without a question, or a single question when `properties.variant`
          is `yes_no`, `rating`, `scale`, `text`, `long_text` or `word_cloud`.
        - `contact_info`: `text_small` and `email` fields; the first name and
          email answers become the response's `name` and `email`.

    SectionInput:
      type: object
      required: [type]
      properties:
        id: { type: string, format: uuid }
        type: { $ref: '#/components/schemas/SectionType' }
        title: { type: string, maxLength: 255 }
        position: { type: integer }
        properties: { $ref: '#/components/schemas/SectionProperties' }
        fields:
          type: array
          items: { $ref: '#/components/schemas/FieldInput' }

    Section:
      allOf:
        - $ref: '#/components/schemas/SectionInput'
        - type: object
          properties:
            fields:
              type: array
              items: { $ref: '#/components/schemas/Field' }

    SectionProperties:
      type: object
      properties:
        description: { type: [string, 'null'] }
        location: { type: [string, 'null'] }
        timezone: { type: [string, 'null'], example: Europe/Berlin }
        layout: { type: string, enum: [default, card] }
        variant:
          type: [string, 'null']
          enum: [yes_no, rating, scale, text, long_text, word_cloud, null]
        redirectUrl: { type: [string, 'null'], format: uri }
        duration: { type: integer, description: 'Meeting: minutes per option, 0 for whole days.' }
        fixedTimezone: { type: boolean }
        allowIfNeedBe: { type: boolean }
        limitToOneOption: { type: boolean }
        optionLimits: { type: boolean }
        hideUnavailableOptions: { type: boolean }
        allowAdditionalVotes: { type: boolean }
        highlightBestOption: { type: boolean }
        finalChoiceId: { type: [string, 'null'], format: uuid }

    FieldType:
      type: string
      enum:
        [
          multiple_choice,
          ranking,
          date,
          yes_no,
          rating,
          scale,
          text_small,
          text_large,
          email,
          phone_number,
          number
        ]

    FieldInput:
      type: object
      required: [type]
      properties:
        id: { type: string, format: uuid }
        type: { $ref: '#/components/schemas/FieldType' }
        label: { type: string, maxLength: 255 }
        position: { type: integer }
        reference: { type: string, description: Your own identifier for this field. }
        properties: { $ref: '#/components/schemas/FieldProperties' }
        choices:
          type: array
          items: { $ref: '#/components/schemas/ChoiceInput' }

    Field:
      allOf:
        - $ref: '#/components/schemas/FieldInput'
        - type: object
          properties:
            responseCount: { type: integer }
            score: { type: string, description: 'Decimal, e.g. Borda points for rankings.' }
            choices:
              type: array
              items: { $ref: '#/components/schemas/Choice' }

    FieldProperties:
      type: object
      description: Which keys apply depends on the field type; others are dropped.
      properties:
        required: { type: boolean, default: true }
        isMultipleChoice: { type: boolean }
        multipleChoiceMin: { type: [integer, 'null'] }
        multipleChoiceMax: { type: [integer, 'null'] }
        randomizeChoices: { type: boolean }
        allowWriteInChoice: { type: boolean }
        listWriteInChoices: { type: boolean }
        optionLimits: { type: boolean }
        showResultsOnSelect: { type: boolean }
        firstPlacePoints: { type: [integer, 'null'] }
        yesLabel: { type: string }
        noLabel: { type: string }
        min: { type: integer }
        max: { type: integer }
        minLabel: { type: string }
        maxLabel: { type: string }
        placeholder: { type: string }

    ChoiceInput:
      type: object
      properties:
        id: { type: string, format: uuid }
        label: { type: string, maxLength: 255 }
        position: { type: integer }
        description: { type: [string, 'null'] }
        mediaId: { type: [string, 'null'], format: uuid }
        maxResponses: { type: [integer, 'null'], description: Seats; null means unlimited. }

    Choice:
      allOf:
        - $ref: '#/components/schemas/ChoiceInput'
        - type: object
          properties:
            responseCount: { type: integer }
            score: { type: string }

    SubmitInput:
      type: object
      required: [fields]
      properties:
        fields:
          type: object
          description: |
            Answers keyed by field id:
            - choice and picture questions: list of choice ids, plus at most one free-text
              string when write-ins are allowed;
            - ranking: every choice id, best first;
            - meeting: per date field, a list of `{ "choiceId", "status" }` with status
              `yes`, `if_need_be` or `no`;
            - `yes_no`: `"yes"` or `"no"`; `rating` and `scale`: the number as a string;
            - text, email and contact fields: a string.
          additionalProperties: true
          example:
            0193e0a2-7b1c-7cc1-9a51-3f0d2c1f8e10: [0193e0a2-7b1c-7cc1-9a51-3f0d2c1f8e11]
        name: { type: string, description: Required for meeting polls. }
        email: { type: string, format: email }
        comment: { type: string, maxLength: 1000 }
        code: { type: string, description: Unique code or invite code. }
        captchaToken: { type: string }
        additional:
          type: boolean
          description: Meeting polls; add another person's vote from the same browser.
        participants:
          type: boolean
          default: true
          description: Send `false` to leave `participants` out of the reply, which is faster on big forms.

    Response:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: [string, 'null'] }
        email: { type: [string, 'null'] }
        phone: { type: [string, 'null'] }
        code: { type: [string, 'null'] }
        countryCode: { type: [string, 'null'] }
        comment: { type: [string, 'null'] }
        fields: { type: object, additionalProperties: true }
        invalidated: { type: boolean }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: [string, 'null'], format: date-time }

    Participant:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: [string, 'null'], description: null unless names are public. }
        comment: { type: [string, 'null'] }
        invalidated: { type: boolean }
        fields: { type: object, additionalProperties: true }

    InviteInput:
      type: object
      required: [email]
      properties:
        name: { type: string }
        email: { type: string, format: email }
        code: { type: string }

    Invite:
      type: object
      properties:
        id: { type: string, format: uuid }
        formId: { type: string, format: uuid }
        inviteListId: { type: [string, 'null'], format: uuid }
        type: { type: string, enum: [email, manual] }
        name: { type: [string, 'null'] }
        email: { type: [string, 'null'] }
        phone: { type: [string, 'null'] }
        code: { type: [string, 'null'] }
        invited: { type: boolean }
        responseId: { type: [string, 'null'], format: uuid }
        createdAt: { type: string, format: date-time }
        respondedAt: { type: [string, 'null'], format: date-time }

    ThemeProperties:
      type: object
      properties:
        backgroundColor: { type: string, example: '#ffffff' }
        textColor: { type: string, example: '#111827' }
        buttonBackgroundColor: { type: string, example: '#4f46e5' }
        buttonTextColor: { type: string, example: '#ffffff' }
        accentColor: { type: string, example: '#4f46e5' }
        successColor: { type: string, example: '#16a34a' }
        warningColor: { type: string, example: '#d97706' }
        errorColor: { type: string, example: '#dc2626' }
        font: { type: string, example: Inter }

    Theme:
      type: object
      properties:
        id: { type: string, format: uuid }
        userId: { type: [string, 'null'], format: uuid }
        name: { type: string }
        properties: { $ref: '#/components/schemas/ThemeProperties' }
        createdAt: { type: string, format: date-time }
