openapi: 3.0.0
info:
  title: Polling.com Account API
  description: |
    Build, manage, and analyse surveys programmatically. This API covers
    survey lifecycle (create, read, update, start, pause, complete, delete),
    respondent access, and aggregate results. All endpoints live under
    `/api/account/*` and authenticate with the same Company API key.

    ## Authentication
    All endpoints require an API key passed via the `token` header.
    Obtain your API key from the Polling.com dashboard under Settings > API.

    ## Plan Requirements
    - **API access** requires the **Standard** plan or higher.
    - **Page elements** (`type: "page"`) require the **Essentials** plan or higher.
    - **Captcha**, **Signature**, and **Click Heatmap** question types require the **Standard** plan or higher.

    ## Rate Limiting
    All endpoints are rate-limited to **60 requests per minute** per API key.
    Exceeding this limit returns HTTP `429 Too Many Requests`.

    ## Question Identifiers and Logic
    Every question the API emits carries a stable 10-char `hash` — the only
    identifier echoed on reads and the one you must pass back to preserve a
    question (and its collected answers) across updates.

    On create you may also supply a human-friendly `id` (e.g. `"screener"`,
    `"age"`) if another question's `logic.target` references it. The server
    resolves `id` → hash internally. If you don't need cross-references, skip
    `id` entirely and the server auto-generates one (`q1`, `q2`, ...). Page
    elements have no `id`.

    **`id` is create-time only.** It is never persisted and never echoed on
    read. Once the survey exists, `logic.target` values must be hashes
    (either an existing hash or the `id` of a new question added in the same
    update payload). Stale `id` references from the create-time payload
    will return `422`.

    Logic rules let you branch, skip, require, or disqualify based on a
    respondent's answer. Each rule specifies an `if` condition, an optional
    `value`, a `then` action, and an optional `target` (question hash) or
    `url`.

    ## Validation
    Use the `/api/account/surveys/validate` endpoint to dry-run your payload before creating a survey.
    Validation returns both errors (blocking) and warnings (non-blocking).

    ## Updating a Survey
    `POST /api/account/surveys/{uuid}` accepts the same payload shape as create and replaces
    the survey's questions and settings. To preserve a question (and the responses already
    collected against it), include the opaque `hash` returned by `GET /api/account/surveys/{uuid}`.
    Questions omitted from the payload are removed; if any removed question has collected
    answers the request fails with `409`. Pass `confirm_delete_questions_with_answers=true`
    in the body to acknowledge the data loss and proceed.

    ## Identifiers
    Questions carry a `hash` (stable 10-char opaque id). That's the only
    identifier the API returns and the one you pass back on update to
    preserve a question (and its collected answers).

    You only need a human-friendly `id` (`"screener"`, `"age"`, etc.) if
    another question's `logic.target` references it at create time — the
    server resolves those refs to hashes internally. If your survey has no
    logic rules, skip `id` entirely; the server will auto-generate one.

    `id` is write-only: it is never echoed on read. On update, match existing
    questions by their `hash`.

    ## Reading Results
    `GET /api/account/surveys/{uuid}/results` returns aggregate stats, per-question
    distributions, and respondent demographics for a survey. The same filters available
    on the dashboard — `country`, `language`, `from`, `to`, `session_status`, `exclude_bots`
    — can be passed as query parameters.
  version: 1.0.0
  contact:
    name: Polling.com API Support
    email: support@polling.com
  license:
    name: MIT License
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://api.polling.com

security:
  - apiKeyAuth: []

paths:
  /api/account/surveys:
    get:
      operationId: listSurveys
      summary: List surveys in the account
      description: |
        Returns every survey in the account, newest first. Each entry carries
        the identity payload (`uuid`, `status`, `share_url`) plus `name`,
        `description`, `created_at`, `updated_at`. Use `GET /surveys/{uuid}`
        to fetch the full question tree for a single survey.
      parameters:
        - name: status
          in: query
          description: Restrict to a single status (case-insensitive).
          schema:
            type: string
            enum: [DRAFT, SCHEDULED, ACTIVE, PAUSED, COMPLETED, DISABLED]
      responses:
        '200':
          description: Surveys in the account, newest first.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    uuid:
                      type: string
                      format: uuid
                    status:
                      type: string
                    share_url:
                      type: string
                      format: uri
                    name:
                      type: string
                    description:
                      type: string
                      nullable: true
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

    post:
      operationId: createSurvey
      summary: Create a survey
      description: |
        Creates a new survey in **draft** status from a simplified JSON payload.
        The payload is validated first; if validation fails, a 422 response is returned
        with the list of errors and no survey is created.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSurveyRequest'
            example:
              name: Customer Satisfaction Q1 2026
              description: Quarterly CSAT survey for enterprise clients
              questions:
                - type: radio
                  id: satisfaction
                  title: How satisfied are you with our service?
                  choices:
                    - Very satisfied
                    - Satisfied
                    - Neutral
                    - Dissatisfied
                    - Very dissatisfied
                  required: true
                  logic:
                    - if: is
                      value: Very dissatisfied
                      then: go_question
                      target: followup
                - type: textarea
                  id: followup
                  title: What could we improve?
                  placeholder: Tell us more...
                - type: nps
                  id: nps_score
                  title: How likely are you to recommend us?
                  rate_max: 10
              settings:
                stop_criteria: responses
                stop_value: 500
                is_anonymous: true
              thank_you_html: "<h2>Thank you for your feedback!</h2>"
      responses:
        '201':
          description: Survey created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSurveyResponse'
              example:
                uuid: 550e8400-e29b-41d4-a716-446655440000
                id: 12345
                status: draft
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Invalid API key.
        '403':
          description: Plan does not support API access (Standard+ required)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: "Your current plan doesn't support API access, please upgrade your account to Standard+."
        '422':
          description: Validation failed — survey was not created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResponse'
              example:
                valid: false
                errors:
                  - path: questions[0].choices
                    message: Choices are required and must be a non-empty array
                    severity: error
        '429':
          description: Rate limit exceeded (60 requests per minute)
        '500':
          description: Unexpected server error
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: An unexpected error occurred.
                  code:
                    type: string
                    description: Error identifier for support reference
                    example: ERR-abc123

  /api/account/surveys/validate:
    post:
      operationId: validateSurvey
      summary: Validate a survey payload
      description: |
        Validates a survey payload without creating anything. Returns whether the payload
        is valid and a list of errors/warnings. Use this to test your payload before calling
        the create endpoint.

        Validation distinguishes between:
        - **error**: Blocks survey creation. Must be fixed.
        - **warning**: Non-blocking. The survey will still be created, but something may not behave as expected.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSurveyRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResponse'
              examples:
                valid:
                  summary: Valid payload with warnings
                  value:
                    valid: true
                    errors:
                      - path: questions[2].title
                        message: Question has no title
                        severity: warning
                invalid:
                  summary: Invalid payload
                  value:
                    valid: false
                    errors:
                      - path: name
                        message: Survey name is required
                        severity: error
                      - path: questions[0].choices
                        message: Choices are required and must be a non-empty array
                        severity: error
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/account/surveys/{uuid}:
    get:
      operationId: getSurvey
      summary: Get a survey in simplified format
      description: |
        Retrieves an existing survey in the same simplified JSON format used
        for creation. The response carries the survey metadata (`uuid`,
        `name`, `description`, `status`, `share_url`, `created_at`,
        `updated_at`) alongside the questions array, settings, and optional
        HTML pages.

        Each question in the response includes an opaque `hash` identifier.
        Pipe the response back into `POST /api/account/surveys/{uuid}` to
        update the survey — keeping `hash` values intact preserves the
        questions (and their collected responses) across the update.
      parameters:
        - name: uuid
          in: path
          required: true
          description: The UUID of the survey to retrieve
          schema:
            type: string
            format: uuid
            example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '200':
          description: Survey data in simplified API format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetSurveyResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found (or does not belong to your account)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Survey not found.

    post:
      operationId: updateSurvey
      summary: Update an existing survey
      description: |
        Replaces the survey's name, description, questions and settings with the supplied
        payload. The payload shape is identical to create.

        **Preserving existing questions.** Each question returned by the GET endpoint
        carries an opaque `hash`. Include that `hash` on the matching question in the
        update payload to keep the underlying question record (and its collected answers).
        Questions without a recognised `hash` are treated as new.

        **Removing questions with answers.** Omitting a question from the payload removes
        it. If any omitted question has collected answers the endpoint returns `409` with
        the offending questions listed — migrate or export that data first.

        The survey `status` is not modified by this endpoint.
      parameters:
        - name: uuid
          in: path
          required: true
          description: The UUID of the survey to update
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSurveyRequest'
      responses:
        '200':
          description: Survey updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSurveyResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: |
            One or more questions you're trying to remove already have collected answers.
            The response lists which questions and how many answers each has.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  questions_with_answers:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        title:
                          type: string
                        answer_count:
                          type: integer
        '422':
          description: Validation failed — the survey was not updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResponse'

    delete:
      operationId: deleteSurvey
      summary: Delete a survey
      description: |
        Permanently deletes the survey along with every session, answer and
        scoped respondent. Irreversible. Prefer `/complete` if you only want
        to stop collecting responses.
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Survey deleted; no body.
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/account/surveys/{uuid}/results:
    get:
      operationId: getSurveyResults
      summary: Aggregate results for a survey
      description: |
        Returns overview stats, per-question summaries, and respondent
        demographics for a survey. The same filters available on the
        dashboard can be passed as query parameters; omitting them returns
        unfiltered totals.

        Each question summary carries the common fields (`hash`, `question`,
        `type`, `total_answers`, `avg_time_seconds`) plus only the
        type-specific fields that apply — see the per-type table on the
        `questions[]` schema below. Text types return a `word_cloud`,
        signature returns signed/not-signed counts, heatmap returns click
        points, and so on.
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: country
          in: query
          description: Filter to sessions from respondents in this ISO country code.
          schema:
            type: string
            example: US
        - name: language
          in: query
          description: Filter to sessions from respondents using this language code.
          schema:
            type: string
            example: en
        - name: from
          in: query
          description: Only count sessions started on or after this date (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: to
          in: query
          description: Only count sessions started on or before this date (YYYY-MM-DD).
          schema:
            type: string
            format: date
        - name: session_status
          in: query
          description: Restrict to a particular session lifecycle bucket.
          schema:
            type: string
            enum:
              - only_completed
              - only_incompleted
              - only_disqualified
              - except_disqualified
        - name: exclude_bots
          in: query
          description: Remove bot-flagged respondents from the results.
          schema:
            type: boolean
      responses:
        '200':
          description: Aggregated results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResultsResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/account/surveys/{uuid}/start:
    post:
      operationId: startSurvey
      summary: Activate a survey
      description: |
        Moves the survey to `ACTIVE` status so respondents can start answering
        through the share link. Allowed from `DRAFT`, `SCHEDULED`, `PAUSED`,
        or already `ACTIVE`. Returns `409` when called on a `COMPLETED` or
        `DISABLED` survey (both are terminal).
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Updated survey identity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSurveyResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Transition is not allowed from the current status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransitionError'

  /api/account/surveys/{uuid}/pause:
    post:
      operationId: pauseSurvey
      summary: Pause a survey
      description: |
        Moves the survey to `PAUSED` status. Existing sessions keep their
        answers; the share link stops accepting new responses. Reversible
        with `/start`. Only `ACTIVE` (or already `PAUSED`) surveys can be
        paused — other statuses return `409`.
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Updated survey identity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSurveyResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Transition is not allowed from the current status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransitionError'

  /api/account/surveys/{uuid}/complete:
    post:
      operationId: completeSurvey
      summary: Close a survey
      description: |
        Moves the survey to `COMPLETED` status — a terminal state. The share
        link stops accepting new responses and the survey cannot be re-opened
        via `/start`. Returns `409` when called on a `DISABLED` survey
        (staff-locked).
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Updated survey identity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSurveyResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Transition is not allowed from the current status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransitionError'

  /api/account/surveys/{uuid}/respondents/list:
    get:
      operationId: listSurveyRespondents
      summary: List respondents who completed a survey
      description: |
        Returns the `external_id` of every respondent who has **at least one
        completed session** for the given survey. Results are capped at 1000
        entries — this endpoint is not paginated. If you need more than that,
        use `sessions-by-respondent` per-id or fall back to your own storage.
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Array of respondent external ids.
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
                  description: The `external_id` supplied when the respondent answered.
                  example: user_123
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/account/surveys/{uuid}/sessions-by-respondent/{external_id}:
    get:
      operationId: getSessionsByRespondent
      summary: Get a respondent's sessions for one survey
      description: |
        Returns every session the respondent has for the given survey, ordered
        newest first, along with the answers recorded on each session.
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: external_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Array of sessions with answers
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Session'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Survey, respondent, or session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/account/respondents/{external_id}:
    get:
      operationId: getRespondent
      summary: Get a respondent's profile + sessions across all surveys
      description: |
        Returns the respondent's demographic profile and every session they have
        across **every** survey in the account — each session entry includes the
        owning `survey_uuid`. Use this for cross-survey analytics on a single
        respondent. For per-survey sessions only, use `sessions-by-respondent`.
      parameters:
        - name: external_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Respondent profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RespondentResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Plan does not support API access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Respondent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: token
      description: Your Polling.com API key. Find it in the dashboard under Settings > API.

  schemas:
    # ------------------------------------------------------------------
    # Request / Response Envelopes
    # ------------------------------------------------------------------

    CreateSurveyRequest:
      type: object
      required:
        - name
        - questions
      properties:
        name:
          type: string
          maxLength: 190
          description: Survey name. Displayed in the dashboard and used as the page title.
          example: Employee Engagement Survey
        description:
          type: string
          nullable: true
          description: Optional survey description. Shown to respondents below the title.
          example: Help us understand how we can improve your workplace experience.
        questions:
          type: array
          minItems: 1
          maxItems: 200
          description: |
            Ordered list of questions and page elements (max 200). Each item must include a `type` field
            that determines which additional fields are available. See the Question schemas below.
          items:
            $ref: '#/components/schemas/Question'
        settings:
          $ref: '#/components/schemas/SurveySettings'
        thank_you_html:
          type: string
          nullable: true
          description: Custom HTML content shown on the thank-you page after survey completion. Supports basic HTML tags.
          example: "<h2>Thank you!</h2><p>Your response has been recorded.</p>"
        disqualified_html:
          type: string
          nullable: true
          description: Custom HTML content shown when a respondent is disqualified (e.g., by a confirmation question or disqualify logic rule).
          example: "<h2>Sorry</h2><p>You do not qualify for this survey.</p>"
        confirm_delete_questions_with_answers:
          type: boolean
          default: false
          description: |
            Update-only. When `true`, acknowledges that any questions being removed which
            already have collected answers should be deleted anyway (data loss). The flag
            has no effect on create. Without it, such updates return `409`.

    CreateSurveyResponse:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the survey. Use this to retrieve or reference the survey.
          example: 550e8400-e29b-41d4-a716-446655440000
        status:
          type: string
          description: The survey status.
          example: draft
        share_url:
          type: string
          format: uri
          description: Public form URL — hand this to respondents to collect answers.
          example: https://app.polling.com/forms/550e8400-e29b-41d4-a716-446655440000/share

    GetSurveyResponse:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: Unique identifier for the survey.
        name:
          type: string
          description: Survey name.
        description:
          type: string
          nullable: true
          description: Survey description.
        status:
          type: string
          description: Current survey status.
          example: draft
        share_url:
          type: string
          format: uri
          description: Public form URL — hand this to respondents to collect answers.
        created_at:
          type: string
          format: date-time
          description: When the survey was created.
        updated_at:
          type: string
          format: date-time
          description: When the survey was last modified.
        questions:
          type: array
          items:
            $ref: '#/components/schemas/Question'
        settings:
          $ref: '#/components/schemas/SurveySettings'
        thank_you_html:
          type: string
          nullable: true
          description: Custom thank-you page HTML.
        disqualified_html:
          type: string
          nullable: true
          description: Custom disqualified page HTML.

    ResultsResponse:
      type: object
      properties:
        survey:
          type: object
          properties:
            uuid:
              type: string
              format: uuid
            name:
              type: string
            status:
              type: string
        stats:
          type: object
          properties:
            sessions:
              type: object
              properties:
                completed:
                  type: integer
                incompleted:
                  type: integer
                disqualified:
                  type: integer
                total:
                  type: integer
            completion_rate_percent:
              type: number
              description: Percentage of sessions that reached completion without disqualifying.
            median_completion_seconds:
              type: integer
              description: Median time (seconds) between session start and completion. `0` when no sessions have completed.
            questions_count:
              type: integer
        demographics:
          type: object
          properties:
            countries:
              type: array
              items:
                type: object
                properties:
                  code:
                    type: string
                    description: ISO-3166 alpha-2 country code (empty string if unresolved).
                  count:
                    type: integer
            languages:
              type: array
              items:
                type: object
                properties:
                  code:
                    type: string
                  count:
                    type: integer
        questions:
          type: array
          description: |
            One entry per question in survey order (pages are not included). Every
            entry carries the common fields below plus **only** the type-specific
            fields that apply — no `null` fillers.

            **Common fields (every entry):**
            - `hash` — opaque stable identifier
            - `question` — the question title
            - `type` — question type
            - `total_answers` — distinct sessions that answered
            - `avg_time_seconds` — average seconds spent on the question

            **Type-specific fields:**

            | Types | Extra fields |
            |---|---|
            | `radio`, `radiogroup`, `dropdown`, `checkbox`, `tagbox`, `boolean`, `confirmation` | `choices: [{value, count, percent}]` |
            | `rating` | `rate_format`, `rate_max`, `avg_rating`, `choices: [{value, count, percent}]` |
            | `nps` | `rate_max`, `avg_score`, `nps_score`, `detractors: {count, percent}`, `passives: {count, percent}`, `promoters: {count, percent}`, `choices: [{value, count, percent}]` |
            | `ranking` | `choices: [{value, avg_rank, position_counts: [n₁, n₂, ...]}]` |
            | `matrix_radio`, `matrix_checkbox`, `matrix_linear` | `matrix: {rows: [{value, columns: [{value, count, percent}]}]}` |
            | `signature` | `signed_count`, `not_signed_count` |
            | `click_heatmap` | `image_url`, `clicks: [{x, y, count}]` |
            | `text`, `textarea`, `comment`, `multipletext`, `address` | `word_cloud: [{word, count}]` (top 20, stopwords removed). Empty array when the question has no answers. |
            | `captcha` and unknown types | no extra fields |

            For checkbox/tagbox `choices[].percent` may sum above 100% — each
            selection is counted independently and the denominator is
            `total_answers` (distinct sessions).
          items:
            type: object
            properties:
              hash:
                type: string
                description: Opaque stable question identifier.
              question:
                type: string
              type:
                type: string
              total_answers:
                type: integer
              avg_time_seconds:
                type: number
        filters_applied:
          type: object
          description: Echo of the filters the server actually applied — useful for debugging and for confirming plan-gated filters (e.g. `exclude_bots`) were honoured.
          properties:
            country:
              type: string
              nullable: true
            language:
              type: string
              nullable: true
            from:
              type: string
              format: date
              nullable: true
            to:
              type: string
              format: date
              nullable: true
            session_status:
              type: string
              nullable: true
            exclude_bots:
              type: boolean

    Session:
      type: object
      description: |
        A single respondent session. When returned from the cross-survey
        respondent endpoint each session also includes `survey_uuid`.
      properties:
        survey_uuid:
          type: string
          format: uuid
          description: Only present on the cross-survey respondent endpoint.
        session_id:
          type: integer
          description: |
            Numeric session identifier. Sessions have no uuid so this is the
            only stable handle if a caller needs to dedupe across polls.
        is_completed:
          type: boolean
        is_disqualified:
          type: boolean
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        answers:
          type: array
          items:
            type: object
            properties:
              question_hash:
                type: string
                description: The question's stable `hash`.
                nullable: true
              question:
                type: string
                description: The question's title.
                nullable: true
              answer:
                type: string
                description: Raw stored answer value. Multi-row answer types (checkbox, ranking) appear as separate entries per session.

    RespondentResponse:
      type: object
      properties:
        external_id:
          type: string
          description: The id you supplied when the respondent answered.
        country:
          type: string
          nullable: true
        region:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        language:
          type: string
          nullable: true
        is_anonymous:
          type: boolean
        is_mobile:
          type: boolean
          nullable: true
        lat:
          type: number
          nullable: true
        lon:
          type: number
          nullable: true
        last_access_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        sessions:
          type: array
          description: Every session this respondent has across all surveys in the account.
          items:
            $ref: '#/components/schemas/Session'

    ValidationResponse:
      type: object
      properties:
        valid:
          type: boolean
          description: |
            `true` if the payload has no errors (warnings are allowed). `false` if there
            are one or more errors that would prevent survey creation.
        errors:
          type: array
          description: List of validation issues found. May include both errors and warnings.
          items:
            $ref: '#/components/schemas/ValidationError'

    ValidationError:
      type: object
      properties:
        path:
          type: string
          description: |
            Dot/bracket path to the field that caused the issue.
            Examples: `"name"`, `"questions[0].choices"`, `"questions[2].logic[0].if"`, `"settings.stop_value"`.
          example: questions[0].choices
        message:
          type: string
          description: Human-readable description of the issue.
          example: Choices are required and must be a non-empty array
        severity:
          type: string
          enum:
            - error
            - warning
          description: |
            - `error`: Blocks survey creation. Must be fixed before the survey can be created.
            - `warning`: Non-blocking. The survey will be created, but the flagged issue may cause unexpected behavior.

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.
          example: Invalid API key.

    TransitionError:
      type: object
      description: Returned by lifecycle endpoints when the requested transition is not legal from the survey's current status.
      properties:
        error:
          type: string
          example: Cannot start a survey in status 'COMPLETED'.
        current_status:
          type: string
          example: COMPLETED
        allowed_from:
          type: array
          description: Statuses from which the requested transition is allowed.
          items:
            type: string

    # ------------------------------------------------------------------
    # Question (discriminated union)
    # ------------------------------------------------------------------

    Question:
      description: |
        A survey element. Use the `type` field to determine which schema applies.
        All question types (except `page`) share these optional common fields:
        - `hash`: Opaque stable identifier — emitted on reads, required on update to preserve existing questions. Read-only on input for *new* questions (omit it; server generates).
        - `id`: **Write-only.** Human-friendly handle used inside `logic.target` at create time. Not echoed on read.
        - `title`: The question text shown to respondents.
        - `helper_text`: Additional help text displayed below the question title.
        - `embed`: Embed code (e.g., YouTube iframe) displayed with the question.
        - `required`: Whether the question must be answered.
        - `default_disabled`: If true, the question starts disabled and must be enabled via logic.
        - `logic`: Array of logic rules for conditional branching.
      discriminator:
        propertyName: type
        mapping:
          radio: '#/components/schemas/RadioQuestion'
          checkbox: '#/components/schemas/CheckboxQuestion'
          ranking: '#/components/schemas/RankingQuestion'
          text: '#/components/schemas/TextQuestion'
          textarea: '#/components/schemas/TextareaQuestion'
          rating: '#/components/schemas/RatingQuestion'
          nps: '#/components/schemas/NpsQuestion'
          boolean: '#/components/schemas/BooleanQuestion'
          confirmation: '#/components/schemas/ConfirmationQuestion'
          matrix_radio: '#/components/schemas/MatrixRadioQuestion'
          matrix_checkbox: '#/components/schemas/MatrixCheckboxQuestion'
          matrix_linear: '#/components/schemas/MatrixLinearQuestion'
          captcha: '#/components/schemas/CaptchaQuestion'
          signature: '#/components/schemas/SignatureQuestion'
          click_heatmap: '#/components/schemas/ClickHeatmapQuestion'
          address: '#/components/schemas/AddressQuestion'
          page: '#/components/schemas/PageElement'
      oneOf:
        - $ref: '#/components/schemas/RadioQuestion'
        - $ref: '#/components/schemas/CheckboxQuestion'
        - $ref: '#/components/schemas/RankingQuestion'
        - $ref: '#/components/schemas/TextQuestion'
        - $ref: '#/components/schemas/TextareaQuestion'
        - $ref: '#/components/schemas/RatingQuestion'
        - $ref: '#/components/schemas/NpsQuestion'
        - $ref: '#/components/schemas/BooleanQuestion'
        - $ref: '#/components/schemas/ConfirmationQuestion'
        - $ref: '#/components/schemas/MatrixRadioQuestion'
        - $ref: '#/components/schemas/MatrixCheckboxQuestion'
        - $ref: '#/components/schemas/MatrixLinearQuestion'
        - $ref: '#/components/schemas/CaptchaQuestion'
        - $ref: '#/components/schemas/SignatureQuestion'
        - $ref: '#/components/schemas/ClickHeatmapQuestion'
        - $ref: '#/components/schemas/AddressQuestion'
        - $ref: '#/components/schemas/PageElement'

    # ------------------------------------------------------------------
    # Shared field definitions (used via allOf)
    # ------------------------------------------------------------------

    QuestionBase:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          description: The question type identifier.
        id:
          type: string
          writeOnly: true
          description: |
            **Only needed if another question's `logic.target` references this
            one.** It's a human-friendly handle you use at create time — e.g.
            write `id: "screener"` on the first question and `logic.target:
            "screener"` on a later one. The server resolves those references
            to hashes.

            Most payloads omit it entirely: if you don't supply `id`, the
            server auto-generates `q1`, `q2`, ... and nothing breaks as long
            as no logic rules depend on a stable handle.

            Never echoed on read — the response always uses `hash` as the
            stable identifier. On update, use `hash` to match existing
            questions; `id` is only for logic refs to *new* questions.
          example: screener
        hash:
          type: string
          pattern: '^[A-Za-z0-9]{10}$'
          description: |
            Opaque identifier used to match a question across updates so its collected
            responses are preserved. Returned by `GET /api/account/surveys/{uuid}`.
            Omit for new questions; pass through unchanged for existing ones. Only
            values previously emitted by GET are accepted — arbitrary strings are
            rejected with `422`. Each `hash` must be unique within a single payload.
          example: aB3xY9kL2q
        title:
          type: string
          description: The question text displayed to the respondent.
          example: What is your favorite color?
        helper_text:
          type: string
          description: Additional help text displayed below the question title in a smaller font.
          example: Select the option that best describes your preference.
        embed:
          type: object
          description: Embedded media displayed alongside the question (e.g., a YouTube video or image).
          properties:
            url:
              type: string
              description: URL of the embedded content.
              example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
            type:
              type: string
              enum:
                - youtube
                - image
              description: The embed type.
              example: youtube
        required:
          type: boolean
          default: false
          description: Whether the respondent must answer this question before proceeding.
        default_disabled:
          type: boolean
          default: false
          description: |
            If `true`, this question starts hidden/disabled. Use an `enable_question` logic
            rule on a preceding question to reveal it conditionally.
        logic:
          type: array
          description: |
            Conditional logic rules evaluated after the respondent answers this question.
            Rules are evaluated in order; the first matching rule is applied.
          items:
            $ref: '#/components/schemas/LogicRule'

    # ------------------------------------------------------------------
    # Choice-Based Questions (radio, checkbox, ranking)
    # ------------------------------------------------------------------

    RadioQuestion:
      description: |
        Single-select question. The respondent picks exactly one choice.
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
            - choices
          properties:
            type:
              type: string
              enum:
                - radio
            choices:
              $ref: '#/components/schemas/ChoiceList'
            randomize:
              type: boolean
              default: false
              description: Randomize the display order of choices for each respondent.
            other_choice:
              type: boolean
              default: false
              description: Show an "Other" free-text option after the defined choices.
            other_choice_text:
              type: string
              description: Custom label for the "Other" option. Only used when `other_choice` is `true`.
              default: Other
              example: Something else...
            image_choices:
              type: boolean
              default: false
              description: |
                Enable image-based choices. When `true`, each item in `choices` must be an
                object with `text` and `image` fields instead of a plain string.

    CheckboxQuestion:
      description: |
        Multi-select question. The respondent can pick one or more choices.
        Supports the same fields as RadioQuestion plus `min_choices` and `max_choices`.
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
            - choices
          properties:
            type:
              type: string
              enum:
                - checkbox
            choices:
              $ref: '#/components/schemas/ChoiceList'
            randomize:
              type: boolean
              default: false
              description: Randomize the display order of choices for each respondent.
            other_choice:
              type: boolean
              default: false
              description: Show an "Other" free-text option after the defined choices.
            other_choice_text:
              type: string
              description: Custom label for the "Other" option. Only used when `other_choice` is `true`.
              default: Other
            image_choices:
              type: boolean
              default: false
              description: Enable image-based choices. When `true`, each choice must be an object with `text` and `image` fields.
            min_choices:
              type: integer
              minimum: 0
              description: Minimum number of choices the respondent must select. Must not exceed `max_choices`.
              example: 1
            max_choices:
              type: integer
              minimum: 1
              description: Maximum number of choices the respondent can select. Must not be less than `min_choices`.
              example: 3

    RankingQuestion:
      description: |
        Drag-to-rank question. The respondent orders all choices by preference.
        Similar to RadioQuestion but without `other_choice` fields (all defined choices must be ranked).
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
            - choices
          properties:
            type:
              type: string
              enum:
                - ranking
            choices:
              $ref: '#/components/schemas/ChoiceList'
            randomize:
              type: boolean
              default: false
              description: Randomize the initial display order of choices.
            image_choices:
              type: boolean
              default: false
              description: Enable image-based choices.

    ChoiceList:
      type: array
      minItems: 1
      description: |
        List of answer choices. Each item can be:
        - A **string** for simple text choices (e.g., `"Red"`)
        - An **object** with `text` and `value` fields if you need distinct display text and stored value
        - An **object** with `text` and `image` fields when `image_choices` is enabled

        When reading back via GET, choice objects where `value === text` are simplified to plain strings.
      items:
        oneOf:
          - type: string
            description: Simple text choice. The display text and stored value are the same.
            example: Red
          - type: object
            description: Choice with separate display text, value, and optional image.
            properties:
              text:
                type: string
                description: Display text shown to the respondent.
                example: Red
              value:
                type: string
                description: Value stored in responses. Defaults to the same as `text` if omitted.
                example: red
              image:
                type: string
                description: URL or base64 data URI of the choice image. Required when `image_choices` is enabled.
                example: https://example.com/images/red.png
            required:
              - text
      example:
        - Strongly agree
        - Agree
        - Neutral
        - Disagree
        - Strongly disagree

    # ------------------------------------------------------------------
    # Text Questions
    # ------------------------------------------------------------------

    TextQuestion:
      description: |
        Single-line text input with configurable input type and validation.

        Logic conditions depend on `input_type`:
        - For `text`, `email`, `url`: `is`, `is_filled`, `is_empty`, `contains`, `doesnt_contains`
        - For `number`, `date`, `time`, `datetime-local`: `is`, `is_filled`, `is_empty`, `between`, `higher`, `lower`
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - text
            input_type:
              type: string
              enum:
                - text
                - number
                - email
                - date
                - time
                - datetime-local
                - url
              default: text
              description: |
                The HTML input type that controls validation and keyboard on mobile devices.
                - `text`: Free-form text input
                - `number`: Numeric input with optional min/max
                - `email`: Email address with format validation
                - `date`: Date picker (YYYY-MM-DD)
                - `time`: Time picker (HH:MM)
                - `datetime-local`: Date and time picker
                - `url`: URL input with format validation
            placeholder:
              type: string
              description: Placeholder text shown inside the empty input field.
              example: Enter your answer here
            min:
              type: string
              description: |
                Minimum value constraint. Interpretation depends on `input_type`:
                - `number`: minimum numeric value
                - `date`/`time`/`datetime-local`: earliest allowed value
                - `text`/`email`/`url`: minimum character length
              example: "0"
            max:
              type: string
              description: |
                Maximum value constraint. Interpretation depends on `input_type`:
                - `number`: maximum numeric value
                - `date`/`time`/`datetime-local`: latest allowed value
                - `text`/`email`/`url`: maximum character length
              example: "100"
            regex:
              type: string
              maxLength: 500
              description: |
                Custom regular expression for validation (max 500 characters). Only effective when `input_type` is `text`.
                A warning is returned if `regex` is set for other input types.
                The pattern must be a valid PCRE regex — syntax is validated server-side.
              example: "^[A-Z]{2}\\d{4}$"

    TextareaQuestion:
      description: |
        Multi-line text input for longer free-form responses.
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains`, `doesnt_contains`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - textarea
            placeholder:
              type: string
              description: Placeholder text shown inside the empty text area.
              example: Share your thoughts...

    # ------------------------------------------------------------------
    # Rating / NPS Questions
    # ------------------------------------------------------------------

    RatingQuestion:
      description: |
        Visual rating question displayed as stars, smileys, or labels.
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `between`, `higher`, `lower`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - rating
            rate_format:
              type: string
              enum:
                - stars
                - smileys
                - labels
              default: stars
              description: |
                Visual format for the rating scale:
                - `stars`: Traditional star icons (1 to N filled stars). Best for satisfaction or quality ratings.
                - `smileys`: Emoji faces ranging from sad to happy. Best for emotional/sentiment feedback.
                - `labels`: Numbered buttons with text labels. Best for Likert-type scales.
            rate_max:
              type: integer
              minimum: 1
              maximum: 20
              default: 5
              description: Maximum rating value (number of stars, smileys, or labels shown).
              example: 5

    NpsQuestion:
      description: |
        Net Promoter Score question. Displays a horizontal numeric scale (0 to `rate_max`).
        The standard NPS scale is 0-10, but a 0-5 variant is also supported.
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `between`, `higher`, `lower`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - nps
            rate_max:
              type: integer
              enum:
                - 5
                - 10
              default: 10
              description: |
                Upper bound of the NPS scale. The scale always starts at 0.
                - `10`: Standard NPS (0-10). Respondents are classified as Detractors (0-6), Passives (7-8), or Promoters (9-10).
                - `5`: Compact NPS variant (0-5).
            rate_format:
              type: string
              enum:
                - stars
                - smileys
                - labels
              default: labels
              description: |
                Visual format for the NPS scale. Same options as RatingQuestion.
                Most NPS implementations use `labels` (numbered buttons).

    # ------------------------------------------------------------------
    # Boolean / Confirmation Questions
    # ------------------------------------------------------------------

    BooleanQuestion:
      description: |
        Yes/No toggle question with customizable labels.
        Supports logic conditions: `is`, `is_filled`, `is_empty`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - boolean
            label_true:
              type: string
              default: "Yes"
              description: Label for the affirmative option.
              example: "Yes"
            label_false:
              type: string
              default: "No"
              description: Label for the negative option.
              example: "No"

    ConfirmationQuestion:
      description: |
        Agree/Disagree gate question, typically used for consent or eligibility screening.
        Automatically injects a disqualify-on-disagree logic rule (respondents who disagree
        are sent to the disqualified page). This built-in rule is not shown in the API output
        and does not need to be included in your payload.

        Custom `logic` rules are allowed and will be **merged** with the auto-injected
        disqualify-on-disagree rule. A warning is returned to alert you of this behavior.
        Supports logic conditions: `is`, `is_filled`, `is_empty`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - confirmation
            label_agree:
              type: string
              default: I agree
              description: Label for the agree button.
              example: I consent
            label_disagree:
              type: string
              default: I disagree
              description: Label for the disagree button.
              example: I do not consent

    # ------------------------------------------------------------------
    # Matrix Questions
    # ------------------------------------------------------------------

    MatrixRadioQuestion:
      description: |
        Matrix question where the respondent selects one column per row (single-select per row).
        Displayed as a grid with rows on the left and columns across the top.
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
            - rows
            - columns
          properties:
            type:
              type: string
              enum:
                - matrix_radio
            rows:
              $ref: '#/components/schemas/MatrixItemList'
            columns:
              $ref: '#/components/schemas/MatrixItemList'

    MatrixCheckboxQuestion:
      description: |
        Matrix question where the respondent can select multiple columns per row (multi-select per row).
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
            - rows
            - columns
          properties:
            type:
              type: string
              enum:
                - matrix_checkbox
            rows:
              $ref: '#/components/schemas/MatrixItemList'
            columns:
              $ref: '#/components/schemas/MatrixItemList'

    MatrixLinearQuestion:
      description: |
        Matrix question with a linear/Likert scale. Each row gets the same set of
        scale points defined by columns (e.g., "Strongly disagree" to "Strongly agree").
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
            - rows
            - columns
          properties:
            type:
              type: string
              enum:
                - matrix_linear
            rows:
              $ref: '#/components/schemas/MatrixItemList'
            columns:
              $ref: '#/components/schemas/MatrixItemList'

    MatrixItemList:
      type: array
      minItems: 1
      description: |
        List of row or column labels. Each item can be:
        - A **string** for simple labels
        - An **object** with `text` and `value` fields for distinct display/stored values

        When reading back via GET, objects where `value === text` are simplified to plain strings.
      items:
        oneOf:
          - type: string
            example: Price
          - type: object
            properties:
              text:
                type: string
                example: Price
              value:
                type: string
                example: price
            required:
              - text
      example:
        - Price
        - Quality
        - Support
        - Ease of use

    # ------------------------------------------------------------------
    # Special Question Types
    # ------------------------------------------------------------------

    CaptchaQuestion:
      description: |
        CAPTCHA verification step. Displays a CAPTCHA challenge to prevent bot submissions.
        This is a verification element, not a data-collection question.

        **Plan requirement**: Standard plan or higher.

        **Logic is not supported** on captcha elements.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - captcha

    SignatureQuestion:
      description: |
        Signature capture question. Displays a canvas where the respondent can draw their signature.

        **Plan requirement**: Standard plan or higher.

        **Logic is not supported** on signature questions.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - signature

    ClickHeatmapQuestion:
      description: |
        Click heatmap question. Displays an image and records where the respondent clicks.
        Useful for UX research, ad testing, and design feedback.

        **Plan requirement**: Standard plan or higher.

        **Logic is not supported** on click heatmap questions.
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
            - image_url
          properties:
            type:
              type: string
              enum:
                - click_heatmap
            image_url:
              type: string
              format: uri
              description: URL of the image to display. The respondent clicks on this image and coordinates are recorded.
              example: https://example.com/images/homepage-screenshot.png

    AddressQuestion:
      description: |
        Structured address input with fields for address lines, city, state, zip code, and country.
        Supports logic conditions: `is`, `is_filled`, `is_empty`, `contains_any`, `doesnt_contains_any`.
        Logic values for address questions reference sub-fields using the AddressLogicField enum (`city`, `state`, `country`).
      allOf:
        - $ref: '#/components/schemas/QuestionBase'
        - type: object
          required:
            - type
          properties:
            type:
              type: string
              enum:
                - address
            address_placeholders:
              type: object
              description: |
                Custom placeholder text for each address sub-field. All fields are optional;
                defaults are used for any omitted fields.
              properties:
                address_line_1:
                  type: string
                  description: Placeholder for the first address line.
                  example: Street address
                address_line_2:
                  type: string
                  description: Placeholder for the second address line.
                  example: Apt, suite, unit, etc.
                city:
                  type: string
                  description: Placeholder for the city field.
                  example: City
                state:
                  type: string
                  description: Placeholder for the state/province field.
                  example: State / Province
                zip_code:
                  type: string
                  description: Placeholder for the zip/postal code field.
                  example: ZIP / Postal code
                country:
                  type: string
                  description: Placeholder for the country field.
                  example: Country

    # ------------------------------------------------------------------
    # Page Element (non-question)
    # ------------------------------------------------------------------

    PageElement:
      type: object
      required:
        - type
      description: |
        A text/HTML page inserted between questions. Used for instructions, consent text,
        section headers, or any informational content. Pages are not questions and do not
        collect responses.

        **Plan requirement**: Essentials plan or higher.

        Pages do not have `id` fields and do not support logic rules.
      properties:
        type:
          type: string
          enum:
            - page
          description: Must be `"page"`.
        content:
          type: string
          description: HTML content to display. Supports standard HTML tags for formatting.
          example: "<h2>Section 2: Demographics</h2><p>The following questions help us understand our audience.</p>"
        hash:
          type: string
          description: |
            Opaque internal identifier returned by GET. Pass through unchanged on update
            to keep the page in place; omit to insert a new page.

    # ------------------------------------------------------------------
    # Logic Rules
    # ------------------------------------------------------------------

    LogicRule:
      type: object
      required:
        - if
        - then
      description: |
        A conditional logic rule evaluated after the respondent answers the parent question.
        Logic rules enable branching, skipping, disqualification, and dynamic question behavior.

        ### How logic works
        1. The respondent answers a question.
        2. Each logic rule on that question is evaluated in order.
        3. The first rule whose `if` condition matches the answer is executed.
        4. The `then` action is applied (e.g., jump to a question, disqualify, open URL).

        ### Condition compatibility by question type
        Not all conditions work with all question types. Using an invalid condition for a
        question type returns a validation error. See each question type's description for
        its supported conditions, or refer to this summary:

        | Condition | radio | checkbox | ranking | text | textarea | rating | nps | boolean | confirmation | matrix_* | address |
        |-----------|-------|----------|---------|------|----------|--------|-----|---------|--------------|----------|---------|
        | `is` | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
        | `is_filled` | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
        | `is_empty` | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
        | `contains_any` | Y | Y | Y | | | | | | | Y | Y |
        | `contains_all` | Y | Y | Y | | | | | | | Y | |
        | `doesnt_contains_any` | Y | Y | Y | | | | | | | Y | Y |
        | `doesnt_contains_all` | Y | Y | Y | | | | | | | Y | |
        | `contains` | | | | Y* | Y | | | | | | |
        | `doesnt_contains` | | | | Y* | Y | | | | | | |
        | `between` | | | | Y** | | Y | Y | | | | |
        | `higher` | | | | Y** | | Y | Y | | | | |
        | `lower` | | | | Y** | | Y | Y | | | | |

        *text questions with `input_type` of `text`, `email`, or `url`
        **text questions with `input_type` of `number`, `date`, `time`, or `datetime-local`

        Logic is **not supported** on: `captcha`, `signature`, `click_heatmap`.
      properties:
        if:
          type: string
          enum:
            - is
            - is_filled
            - is_empty
            - contains_any
            - contains_all
            - doesnt_contains_any
            - doesnt_contains_all
            - contains
            - doesnt_contains
            - between
            - higher
            - lower
          description: |
            The condition to evaluate against the respondent's answer.
            - `is`: Answer exactly equals the value.
            - `is_filled`: Answer is not empty (no `value` needed).
            - `is_empty`: Answer is empty (no `value` needed).
            - `contains_any`: Answer contains at least one of the values in the array.
            - `contains_all`: Answer contains all of the values in the array.
            - `doesnt_contains_any`: Answer does not contain any of the values in the array.
            - `doesnt_contains_all`: Answer does not contain all of the values in the array.
            - `contains`: Answer text contains the substring (for text/textarea).
            - `doesnt_contains`: Answer text does not contain the substring (for text/textarea).
            - `between`: Answer is between `value.from` and `value.to` (inclusive, for numeric/date/time).
            - `higher`: Answer is greater than the value (for numeric/date/time).
            - `lower`: Answer is less than the value (for numeric/date/time).
        value:
          description: |
            The value to compare against. Shape depends on the `if` condition:
            - `is`, `contains`, `doesnt_contains`, `higher`, `lower`: a single string or number.
            - `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`: an array of strings.
            - `between`: an object with `from` and `to` fields.
            - `is_filled`, `is_empty`: not required (omit or set to null).
          oneOf:
            - type: string
              description: Single value for `is`, `contains`, `doesnt_contains`, `higher`, `lower`.
              example: Very dissatisfied
            - type: number
              description: Numeric value for `higher`, `lower`, or `is` on rating/NPS questions.
              example: 6
            - type: array
              description: Array of values for `contains_any`, `contains_all`, `doesnt_contains_any`, `doesnt_contains_all`.
              items:
                type: string
              example:
                - Red
                - Blue
            - $ref: '#/components/schemas/BetweenValue'
        then:
          type: string
          enum:
            - go_question
            - make_required
            - make_not_required
            - disable_question
            - enable_question
            - finish
            - disqualify
            - open_url
          description: |
            The action to perform when the condition matches.
            - `go_question`: Jump to the question specified by `target`, skipping questions in between.
            - `make_required`: Make the question specified by `target` required.
            - `make_not_required`: Make the question specified by `target` not required.
            - `disable_question`: Hide/disable the question specified by `target`.
            - `enable_question`: Show/enable the question specified by `target` (use with `default_disabled`).
            - `finish`: End the survey immediately and show the thank-you page.
            - `disqualify`: End the survey and show the disqualified page.
            - `open_url`: Redirect the respondent to the URL specified in `url`.
        target:
          type: string
          description: |
            Identifier of the target question. Required for `go_question`,
            `make_required`, `make_not_required`, `disable_question`, and
            `enable_question`. On **input** you may pass either a human-friendly
            `id` (e.g. `"q3"`, `"satisfaction"`) or the target's `hash`; the
            server resolves either. On **output** the server always emits the
            target's `hash`, so round-trip GET → POST requires no translation.
          example: aB3xY9kL2q
        url:
          type: string
          format: uri
          description: |
            The URL to redirect to. Required for the `open_url` action.
            Must start with `http://` or `https://`.
          example: https://example.com/thank-you

    BetweenValue:
      type: object
      required:
        - from
        - to
      description: Range value for the `between` logic condition.
      properties:
        from:
          oneOf:
            - type: string
            - type: number
          description: Lower bound of the range (inclusive).
          example: 1
        to:
          oneOf:
            - type: string
            - type: number
          description: Upper bound of the range (inclusive).
          example: 5

    # ------------------------------------------------------------------
    # Survey Settings
    # ------------------------------------------------------------------

    SurveySettings:
      type: object
      description: |
        Configuration settings for the survey. All fields are optional and have sensible defaults.
      properties:
        stop_criteria:
          type: string
          enum:
            - forever
            - datetime
            - responses
          default: forever
          description: |
            When to stop accepting responses:
            - `forever`: The survey stays open indefinitely.
            - `datetime`: Close the survey at a specific date/time (set `stop_value` to an ISO 8601 datetime).
            - `responses`: Close the survey after reaching a response count (set `stop_value` to an integer).
        stop_value:
          oneOf:
            - type: string
              description: ISO 8601 datetime for `datetime` stop criteria.
              example: "2026-12-31T23:59:59Z"
            - type: integer
              description: Number of responses for `responses` stop criteria.
              example: 1000
          nullable: true
          description: |
            The value associated with `stop_criteria`. Required when `stop_criteria` is not `"forever"`.
            - For `datetime`: an ISO 8601 datetime string.
            - For `responses`: an integer response count.
        start_trigger:
          type: string
          enum:
            - now
            - scheduled
          default: now
          description: |
            When the survey becomes active:
            - `now`: The survey is available immediately after creation (in draft status; publish separately).
            - `scheduled`: The survey activates at the time specified by `start_at`.
        start_at:
          type: string
          format: date-time
          nullable: true
          description: ISO 8601 datetime for when the survey should activate. Required when `start_trigger` is `"scheduled"`.
          example: "2026-04-01T09:00:00Z"
        is_anonymous:
          type: boolean
          default: false
          description: |
            If `true`, respondent identifiers are not stored. Results are fully anonymous.
        redirect_url:
          type: string
          format: uri
          nullable: true
          description: URL to redirect respondents to after completion. Required when `redirect_type` is `"custom"`. Must start with `http://` or `https://`.
          example: https://example.com/thank-you
        redirect_type:
          type: string
          enum:
            - none
            - custom
            - results
          default: none
          description: |
            Post-completion redirect behavior:
            - `none`: Show the default thank-you page (or `thank_you_html` if set).
            - `custom`: Redirect to `redirect_url`.
            - `results`: Redirect to the public results page for this survey.
        redirect_url_disqualified:
          type: string
          format: uri
          nullable: true
          description: URL to redirect disqualified respondents to instead of showing the disqualified page. Must start with `http://` or `https://`.
          example: https://example.com/sorry
        user_hash_method:
          type: string
          enum:
            - ip
            - cookie
            - none
          default: cookie
          description: |
            Controls the dashboard's **Restrict Repeat Submissions** setting — how the
            survey decides whether a respondent is a duplicate:
            - `cookie`: Use a browser cookie. Most reliable for public web surveys.
            - `ip`: Use the respondent's IP address. Useful for kiosk or shared-device scenarios.
            - `none`: No deduplication. Same person / IP / browser can submit repeatedly.
        reward_amount:
          type: number
          nullable: true
          description: Reward amount offered to respondents for completing the survey (e.g., gift card value).
          example: 5.00
        reward_name:
          type: string
          nullable: true
          description: Name/description of the reward (e.g., "Amazon Gift Card").
          example: Amazon Gift Card
        complete_html:
          type: string
          nullable: true
          description: |
            Custom HTML rendered on the completion page instead of the default thank-you
            message. Alias-compatible with `thank_you_html` on the survey payload — either
            can be used; both end up here.
          example: "<h2>Thanks!</h2><p>We'll be in touch shortly.</p>"
        complete_json:
          type: object
          nullable: true
          description: |
            Arbitrary JSON object surfaced on the completion page (used by the dashboard
            for structured completion-screen configuration). Leave `null` unless you know
            you need it.
        screenout_enabled:
          type: boolean
          default: false
          description: |
            Enable geographic and language-based screening. When enabled, respondents
            outside the allowed countries or languages are automatically screened out.
        screenout_countries:
          type: array
          items:
            type: string
            description: ISO 3166-1 alpha-2 country code.
            example: US
          default: []
          description: |
            List of allowed country codes (ISO 3166-1 alpha-2). Only respondents from
            these countries can take the survey. Requires `screenout_enabled: true`.
          example:
            - US
            - GB
            - CA
        screenout_languages:
          type: array
          items:
            type: string
            description: ISO 639-1 language code.
            example: en
          default: []
          description: |
            List of allowed language codes (ISO 639-1). Only respondents with matching
            browser language can take the survey. Requires `screenout_enabled: true`.
          example:
            - en
            - fr
