openapi: 3.1.0
info:
  title: Busch Labs Study Configuration API
  version: "1.0"
  description: |
    Configure and price UX research studies. No auth required (public beta).
    The API is free; pricing reflects the cost of the study you configure.
    Rate limits: 30 price checks/hour, 5 submissions/hour per IP.
  termsOfService: https://www.busch-labs.at/legal/privacy
  contact:
    name: Busch Labs
    url: https://www.busch-labs.at
  license:
    name: Proprietary
    url: https://www.busch-labs.at/legal/impressum

security: []

externalDocs:
  description: API Documentation
  url: https://api.busch-labs.at/docs

servers:
  - url: https://api.busch-labs.at
    description: Production

tags:
  - name: Study Configuration
    description: Configure, price, and submit UX research studies

paths:
  /study-config:
    get:
      operationId: getStudyConfigSchema
      summary: Schema, options, and examples
      description: Returns JSON Schema, valid enum values, geo-targeting options, example payloads, and rate limit info for constructing valid POST requests.
      tags: [Study Configuration]
      responses:
        "200":
          description: Schema and options for study configuration
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, max-age=3600, s-maxage=86400"
          content:
            application/json:
              schema:
                type: object
                required: [name, description, endpoint, schema, options, examples, rateLimit, geoCascadeRules]
                properties:
                  name:
                    type: string
                  description:
                    type: string
                  endpoint:
                    type: string
                  schema:
                    type: object
                    description: JSON Schemas for price check and submit input.
                  options:
                    type: object
                    description: All valid enum values with descriptions.
                  examples:
                    type: array
                    description: Example request bodies.
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        body:
                          type: object
                  rateLimit:
                    type: object
                    properties:
                      priceCheck:
                        type: string
                      submit:
                        type: string
                  geoCascadeRules:
                    type: object
                    description: Geo filter cascade rules and available levels.

    post:
      operationId: postStudyConfig
      summary: Price check or submit a study order
      description: Calculates pricing and feasibility. With ?submit=true, also submits the order (requires clientName, clientEmail, source).
      tags: [Study Configuration]
      parameters:
        - name: submit
          in: query
          required: false
          description: Set to true to submit a study order (requires contact fields).
          schema:
            type: string
            enum: ["true"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Study configuration. For submit mode (?submit=true), also include clientName, clientEmail, and source.
              required:
                - persona
                - method
                - location
                - serviceMode
                - participantCount
                - durationMinutes
              properties:
                persona:
                  type: string
                  enum: [b2c_general, b2c_gamer, b2b_professional, b2b_decision_maker]
                  description: Target audience type.
                method:
                  type: string
                  enum: [survey, interview, user_test]
                  description: Research method.
                location:
                  type: string
                  enum: [remote, in_person_client, in_person_lab, in_home]
                  description: Study location type.
                serviceMode:
                  type: string
                  enum: [connect, platform, expert]
                  description: Service level (connect=recruiting only, platform=+tools, expert=+human services).
                participantCount:
                  type: integer
                  minimum: 0
                  maximum: 500
                durationMinutes:
                  type: integer
                  minimum: 1
                  maximum: 360
                aiInterviewer:
                  type: boolean
                  default: false
                  description: Only applies when serviceMode=platform.
                expertAnalysis:
                  type: boolean
                  default: false
                  description: Applies when serviceMode=platform or expert.
                expertStudyDesign:
                  type: boolean
                  default: false
                  description: Only applies when serviceMode=expert.
                expertModeration:
                  type: boolean
                  default: false
                  description: Only applies when serviceMode=expert.
                consultingAddon:
                  type: boolean
                  default: false
                consultingHours:
                  type: integer
                  minimum: 0
                  default: 0
                customScreening:
                  type: boolean
                  default: false
                qualificationRate:
                  type: number
                  minimum: 10
                  maximum: 90
                  default: 50
                screeningQuestions:
                  type: array
                  default: []
                  items:
                    type: object
                    required: [id, text]
                    properties:
                      id:
                        type: string
                      text:
                        type: string
                selectedFilters:
                  type: object
                  default: {}
                  description: Map of filter key to selected option IDs (e.g. geo_countries, geo_regions).
                  additionalProperties:
                    type: array
                    items:
                      type: string
                targetGroups:
                  type: array
                  default: []
                  description: Multi-group mode. Omit or pass [] for single-group.
                  items:
                    type: object
                    required: [id, participantCount, selectedFilters, filterPrefills]
                    properties:
                      id:
                        type: string
                      participantCount:
                        type: integer
                        minimum: 1
                        maximum: 500
                      selectedFilters:
                        type: object
                        additionalProperties:
                          type: array
                          items:
                            type: string
                      filterPrefills:
                        type: object
                        additionalProperties:
                          type: string
                          enum: [location, lab]
                locationCountry:
                  type: string
                  default: ""
                locationPostalCode:
                  type: string
                  default: ""
                deliveryStartDate:
                  type: string
                  format: date
                  description: Optional delivery start date (YYYY-MM-DD) or null.
                deliveryEndDate:
                  type: string
                  format: date
                  description: Optional delivery end date (YYYY-MM-DD) or null.
                expertSubCategory:
                  type: string
                  enum: [healthcare, business, finance, legal]
                  description: Optional expert sub-category.
                gameAccessType:
                  type: string
                  enum: [steam, other_platform]
                  description: Optional game access type for gamer persona.
                gameAccessLink:
                  type: string
                  default: ""
                assetType:
                  type: string
                  enum: [url_figma, mobile_app, pc_mac_software, custom_physical]
                  description: Optional asset type for user tests.
                assetUrl:
                  type: string
                  default: ""
                studyTitle:
                  type: string
                  default: ""
                studyDescription:
                  type: string
                  default: ""
                clientName:
                  type: string
                  minLength: 2
                  maxLength: 200
                  description: Required for submit mode (?submit=true).
                clientEmail:
                  type: string
                  format: email
                  description: Required for submit mode (?submit=true).
                additionalNotes:
                  type: string
                  default: ""
                  description: Optional notes (submit mode only).
                preferredLocale:
                  type: string
                  enum: [en, de]
                  default: en
                  description: Preferred response locale (submit mode only).
                source:
                  type: string
                  enum: [calculator, player-labs-calculator, api]
                  description: Required for submit mode (?submit=true).
              additionalProperties: false
            examples:
              minimal:
                summary: Minimal 10-min remote survey with 100 consumers
                value:
                  persona: b2c_general
                  method: survey
                  location: remote
                  serviceMode: connect
                  participantCount: 100
                  durationMinutes: 10
              withGeoFilter:
                summary: 30-min interview, Austrian participants only
                value:
                  persona: b2c_general
                  method: interview
                  location: remote
                  serviceMode: platform
                  participantCount: 15
                  durationMinutes: 30
                  selectedFilters:
                    geo_countries: ["AT"]
                    geo_regions: ["AT_W", "AT_NO"]
              multiGroup:
                summary: Two target groups with different filters
                value:
                  persona: b2b_professional
                  method: interview
                  location: remote
                  serviceMode: expert
                  participantCount: 20
                  durationMinutes: 45
                  expertAnalysis: true
                  selectedFilters: {}
                  targetGroups:
                    - id: group-a
                      participantCount: 10
                      selectedFilters:
                        geo_countries: ["AT"]
                        industry: ["technology"]
                      filterPrefills: {}
                    - id: group-b
                      participantCount: 10
                      selectedFilters:
                        geo_countries: ["DE"]
                        industry: ["finance"]
                      filterPrefills: {}
              submitExample:
                summary: Submit a study order with contact info
                value:
                  persona: b2c_general
                  method: survey
                  location: remote
                  serviceMode: connect
                  participantCount: 50
                  durationMinutes: 15
                  clientName: Jane Doe
                  clientEmail: jane@example.com
                  source: api
      responses:
        "200":
          description: Pricing and feasibility result
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    required: [pricing, feasibility]
                    properties:
                      pricing:
                        type: object
                        description: Full pricing breakdown.
                        required: [grandTotal, calculatedAt, schemaVersion]
                        properties:
                          lineItems:
                            type: array
                            items:
                              type: object
                              required: [key, label, amount, perParticipant, category]
                              properties:
                                key:
                                  type: string
                                label:
                                  type: string
                                amount:
                                  type: number
                                  description: Amount in euros.
                                perParticipant:
                                  type: boolean
                                quantity:
                                  type: integer
                                unitPrice:
                                  type: number
                                category:
                                  type: string
                                  enum: [recruiting, platform, expert, screening, infrastructure]
                          subtotalPerParticipant:
                            type: number
                            description: Cost per participant (euros).
                          subtotalParticipants:
                            type: number
                            description: Total participant cost (euros).
                          aiInterviewerTotal:
                            type: number
                          expertServicesTotal:
                            type: number
                          consultingTotal:
                            type: number
                          screeningTotal:
                            type: number
                          labFacilityTotal:
                            type: number
                          grandTotal:
                            type: number
                            description: Total price in euros (excl. VAT).
                          requiresCustomQuote:
                            type: boolean
                            description: True when expert analysis for surveys >200 responses.
                          input:
                            type: object
                            description: Echo of the pricing engine input.
                          calculatedAt:
                            type: string
                            format: date-time
                          schemaVersion:
                            type: string
                      feasibility:
                        type: object
                        description: Recruitment feasibility assessment.
                        required: [incidenceRate, level, perField]
                        properties:
                          incidenceRate:
                            type: number
                            description: Combined incidence rate (0-1).
                          level:
                            type: string
                            enum: [easy, medium, hard]
                          perField:
                            type: object
                            description: Incidence rate per filter field.
                      perGroup:
                        type: array
                        description: Per-group feasibility (present when targetGroups has 2+ entries).
                        items:
                          type: object
                          properties:
                            groupId:
                              type: string
                            participantCount:
                              type: integer
                            feasibility:
                              type: object
                              properties:
                                incidenceRate:
                                  type: number
                                level:
                                  type: string
                                  enum: [easy, medium, hard]
                                perField:
                                  type: object
                      claimToken:
                        type: string
                        description: Token to claim the submitted study order (submit mode only).
                      emailSent:
                        type: boolean
                        description: Whether confirmation email was delivered (submit mode only).
        "400":
          description: Invalid JSON body
          content:
            application/json:
              schema:
                type: object
                required: [success, error]
                properties:
                  success:
                    type: boolean
                  error:
                    type: object
                    required: [code, message]
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                success: false
                error:
                  code: INVALID_JSON
                  message: Request body must be valid JSON
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                type: object
                required: [success, error]
                properties:
                  success:
                    type: boolean
                  error:
                    type: object
                    required: [code, message]
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      details:
                        type: array
                        items:
                          type: object
                          required: [path, message]
                          properties:
                            path:
                              type: string
                            message:
                              type: string
              example:
                success: false
                error:
                  code: VALIDATION_ERROR
                  message: Invalid study configuration
                  details:
                    - path: persona
                      message: "Invalid enum value. Expected 'b2c_general' | 'b2c_gamer' | 'b2b_professional' | 'b2b_decision_maker'"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                type: object
                required: [success, error]
                properties:
                  success:
                    type: boolean
                  error:
                    type: object
                    required: [code, message]
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                success: false
                error:
                  code: RATE_LIMITED
                  message: Too many requests
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                type: object
                required: [success, error]
                properties:
                  success:
                    type: boolean
                  error:
                    type: object
                    required: [code, message]
                    properties:
                      code:
                        type: string
                      message:
                        type: string
              example:
                success: false
                error:
                  code: INTERNAL_ERROR
                  message: An unexpected error occurred
