openapi: 3.1.0
info:
  title: BuoyForms API
  description: |
    REST API for programmatic access to BuoyForms data. Create, manage, and collect
    form submissions with full API access.

    ## Authentication

    Authenticated `/api/v1` endpoints require an API key. You can generate API keys
    from your organization settings at `/app/settings/api-keys`.

    API keys are prefixed with `bf_` and should be kept secret. The full key is only
    shown once at creation time and cannot be retrieved later.

    Include your API key in requests using one of these methods:

    - **Authorization header**: `Authorization: Bearer bf_your_api_key_here`
    - **X-API-Key header**: `X-API-Key: bf_your_api_key_here`

    ### Example

    ```bash
    curl -H "Authorization: Bearer bf_abc123..." \
      https://buoyforms.com/api/v1/forms
    ```

    Public hosted/embed form runtime endpoints under `/api/public` do not require API keys.
    Those endpoints are rate-limited separately and may require a `captchaToken` when the
    form enables public bot protection.

    ## Rate Limiting

    Authenticated API requests are rate-limited per API key based on your subscription plan:

    | Plan       | Requests per minute |
    |------------|---------------------|
    | Free       | 10                  |
    | Pro        | 100                 |
    | Business   | 500                 |
    | Enterprise | 2,000               |

    Rate limit headers are included in all responses:
    - `X-RateLimit-Limit`: Maximum requests per window
    - `X-RateLimit-Remaining`: Requests remaining in current window
    - `X-RateLimit-Reset`: Unix timestamp when the window resets
    - `Retry-After`: Seconds until you can retry (only present when rate limited)

    ## Scopes

    API keys can be configured with specific scopes to limit access:
    - `*` - Full API access for the organization
    - `forms:*` - Read and write all form resources
    - `forms:read` - Read form definitions and metadata
    - `forms:write` - Create and update forms
    - `submissions:*` - Read and write all submission resources
    - `submissions:read` - Read form submissions
    - `submissions:write` - Create new submissions programmatically
    - `webhooks:*` - Read and manage all webhook resources
    - `webhooks:read` - Read webhook configurations
    - `webhooks:write` - Create and manage webhooks

    Each endpoint documents which scope is required. If your API key lacks the necessary
    scope, the request returns `403 Forbidden` with the required scope in the error message.
    Namespace wildcards like `forms:*` and the global wildcard `*` also satisfy the
    corresponding endpoint requirements.

    ## Pagination

    List endpoints support pagination via `limit` and `offset` query parameters.
    Responses include a `pagination` object with `limit`, `offset`, and `total` fields
    so you can calculate how many pages remain.

    ## Errors

    All error responses follow a consistent format with `error` (machine-readable code)
    and `message` (human-readable description) fields. See the Error schema for details.

  version: 1.0.0
  contact:
    name: BuoyForms Support
    email: support@buoyforms.com
  license:
    name: Proprietary
    url: https://buoyforms.com/terms

servers:
  - url: /api/v1
    description: Production API

security:
  - BearerAuth: []
  - ApiKeyAuth: []

tags:
  - name: Forms
    description: |
      Endpoints for listing and retrieving form definitions. Requires the `forms:read` scope.
  - name: Submissions
    description: |
      Endpoints for listing, retrieving, and creating form submissions.
      Read operations require `submissions:read`, write operations require `submissions:write`.
  - name: Public Forms
    description: |
      Unauthenticated endpoints used by hosted and embedded public forms.
      These endpoints do not require API keys. When a form enables public bot protection,
      submission requests must include a `captchaToken` from the public form runtime.

paths:
  /forms:
    get:
      tags:
        - Forms
      summary: List forms
      description: |
        Retrieve a list of all forms in your organization, excluding soft-deleted forms.
        Results are sorted by last updated date (newest first).

        Returns up to 100 forms. For organizations with more forms, results are truncated.

        **Required scope:** `forms:read`
      operationId: listForms
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      responses:
        '200':
          description: List of forms returned successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - forms
                properties:
                  forms:
                    type: array
                    items:
                      $ref: '#/components/schemas/FormSummary'
              example:
                forms:
                  - id: "form_abc123"
                    title: "Contact Form"
                    description: "General contact form for website visitors"
                    status: "published"
                    createdAt: "2025-01-15T10:30:00.000Z"
                    updatedAt: "2025-01-20T14:45:00.000Z"
                  - id: "form_def456"
                    title: "Newsletter Signup"
                    description: null
                    status: "draft"
                    createdAt: "2025-01-18T09:00:00.000Z"
                    updatedAt: "2025-01-18T09:00:00.000Z"
                  - id: "form_ghi789"
                    title: "Customer Feedback"
                    description: "Post-purchase satisfaction survey"
                    status: "archived"
                    createdAt: "2024-11-01T12:00:00.000Z"
                    updatedAt: "2025-01-10T08:30:00.000Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenFormsRead'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /forms/{formId}:
    get:
      tags:
        - Forms
      summary: Get form details
      description: |
        Retrieve detailed information about a specific form, including all field
        definitions with their types, labels, validation rules, and display order.

        The form must belong to your organization. Soft-deleted forms are not returned.

        **Required scope:** `forms:read`
      operationId: getForm
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/formId'
      responses:
        '200':
          description: Form details returned successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - form
                properties:
                  form:
                    $ref: '#/components/schemas/FormDetail'
              example:
                form:
                  id: "form_abc123"
                  title: "Contact Form"
                  description: "General contact form for website visitors"
                  status: "published"
                  createdAt: "2025-01-15T10:30:00.000Z"
                  updatedAt: "2025-01-20T14:45:00.000Z"
                  fields:
                    - id: "fld_email01"
                      type: "email"
                      label: "Email Address"
                      placeholder: "you@example.com"
                      description: "We'll use this to respond to your inquiry"
                      required: true
                      metadata: {}
                      order: 0
                    - id: "fld_name01"
                      type: "text"
                      label: "Full Name"
                      placeholder: "Jane Doe"
                      description: null
                      required: true
                      metadata: {}
                      order: 1
                    - id: "fld_msg01"
                      type: "textarea"
                      label: "Message"
                      placeholder: "How can we help?"
                      description: null
                      required: false
                      metadata: {}
                      order: 2
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenFormsRead'
        '404':
          $ref: '#/components/responses/FormNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /forms/{formId}/structure:
    put:
      tags:
        - Forms
      summary: Replace form structure
      description: |
        Replace all pages and fields on an existing form. This is an atomic operation:
        all existing pages and fields are deleted and replaced with the provided ones.

        Use this to update a form's complete structure programmatically, e.g., from an
        LLM-generated form definition.

        **Required scope:** `forms:write`
      operationId: updateFormStructure
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/formId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - pages
                - fields
              properties:
                pages:
                  type: array
                  items:
                    $ref: '#/components/schemas/FormPage'
                fields:
                  type: array
                  items:
                    $ref: '#/components/schemas/FormField'
                settings:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Form structure replaced successfully.
          content:
            application/json:
              schema:
                type: object
                required:
                  - form
                properties:
                  form:
                    $ref: '#/components/schemas/FormDetail'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenFormsWrite'
        '404':
          $ref: '#/components/responses/FormNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /forms/{formId}/submissions:
    get:
      tags:
        - Submissions
      summary: List form submissions
      description: |
        Retrieve submissions for a specific form. Results are paginated and sorted by
        submission date (newest first).

        Each submission includes the submitted data as a `data` object where keys are
        field IDs and values are the submitted values.

        The form must belong to your organization.

        **Required scope:** `submissions:read`
      operationId: listSubmissions
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/formId'
        - name: limit
          in: query
          description: |
            Maximum number of submissions to return per page.
            Minimum 1, maximum 100, default 50.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          example: 25
        - name: offset
          in: query
          description: |
            Number of submissions to skip for pagination. Use this with `limit`
            to page through results.
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
      responses:
        '200':
          description: Paginated list of submissions returned successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - submissions
                  - pagination
                properties:
                  submissions:
                    type: array
                    items:
                      $ref: '#/components/schemas/SubmissionSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
              example:
                submissions:
                  - id: "sub_xyz789"
                    submitterEmail: "john@example.com"
                    submitterName: "John Doe"
                    createdAt: "2025-01-22T11:30:00.000Z"
                    data:
                      fld_email01: "john@example.com"
                      fld_name01: "John Doe"
                      fld_msg01: "Hello, I have a question about pricing."
                  - id: "sub_abc456"
                    submitterEmail: "jane@example.com"
                    submitterName: "Jane Smith"
                    createdAt: "2025-01-21T09:15:00.000Z"
                    data:
                      fld_email01: "jane@example.com"
                      fld_name01: "Jane Smith"
                      fld_msg01: "I'd like to schedule a demo."
                pagination:
                  limit: 50
                  offset: 0
                  total: 127
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSubmissionsRead'
        '404':
          $ref: '#/components/responses/FormNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    post:
      tags:
        - Submissions
      summary: Create submission
      description: |
        Programmatically submit data to a form. This is useful for integrations,
        migrations, or submitting data from custom frontends.

        The form must be in `published` status to accept submissions. Required fields
        (as defined in the form) must be included in the `data` object.

        On success, this endpoint:
        - Creates the submission record
        - Fires any configured webhooks (`form.submission.created` event)
        - Triggers CRM integrations (Salesforce, HubSpot, etc.)
        - Increments your monthly submission usage counter

        **Required scope:** `submissions:write`
      operationId: createSubmission
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/formId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubmissionRequest'
            examples:
              contact_form:
                summary: Contact form submission
                value:
                  data:
                    fld_email01: "john@example.com"
                    fld_name01: "John Doe"
                    fld_msg01: "Hello, I would like to learn more about your services."
                  submitterEmail: "john@example.com"
                  submitterName: "John Doe"
              minimal:
                summary: Minimal submission (data only)
                value:
                  data:
                    fld_email01: "user@example.com"
      responses:
        '201':
          description: Submission created successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - submission
                properties:
                  submission:
                    $ref: '#/components/schemas/CreatedSubmission'
              example:
                submission:
                  id: "sub_abc123xyz"
                  formId: "form_abc123"
                  createdAt: "2025-01-22T15:30:00.000Z"
        '400':
          description: |
            Bad request. The request body is invalid, missing required fields,
            or the form is not in a published state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_json:
                  summary: Invalid JSON body
                  value:
                    error: "invalid_json"
                    message: "Invalid JSON in request body"
                invalid_data:
                  summary: Missing data object
                  value:
                    error: "invalid_data"
                    message: 'Request body must include a "data" object'
                missing_fields:
                  summary: Missing required fields
                  value:
                    error: "missing_required_fields"
                    message: "Missing required fields: Email Address, Full Name"
                form_not_published:
                  summary: Form is not published
                  value:
                    error: "form_not_published"
                    message: "Form is not published"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Monthly submission limit reached for your plan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "limit_exceeded"
                message: "Monthly submission limit reached. Please upgrade your plan."
        '403':
          $ref: '#/components/responses/ForbiddenSubmissionsWrite'
        '404':
          $ref: '#/components/responses/FormNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /submissions/{submissionId}:
    get:
      tags:
        - Submissions
      summary: Get submission details
      description: |
        Retrieve detailed information about a specific submission, including all
        submitted field values.

        The response includes two representations of the submitted data:
        - `data`: Field labels as keys (human-readable, e.g. `"Email Address": "john@example.com"`)
        - `rawData`: Field IDs as keys (stable identifiers, e.g. `"fld_email01": "john@example.com"`)

        The submission must belong to your organization.

        **Required scope:** `submissions:read`
      operationId: getSubmission
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/submissionId'
      responses:
        '200':
          description: Submission details returned successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                type: object
                required:
                  - submission
                properties:
                  submission:
                    $ref: '#/components/schemas/SubmissionDetail'
              example:
                submission:
                  id: "sub_xyz789"
                  formId: "form_abc123"
                  formTitle: "Contact Form"
                  submitterEmail: "john@example.com"
                  submitterName: "John Doe"
                  createdAt: "2025-01-22T11:30:00.000Z"
                  data:
                    "Email Address": "john@example.com"
                    "Full Name": "John Doe"
                    "Message": "Hello, I have a question about pricing."
                  rawData:
                    fld_email01: "john@example.com"
                    fld_name01: "John Doe"
                    fld_msg01: "Hello, I have a question about pricing."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenSubmissionsRead'
        '404':
          $ref: '#/components/responses/SubmissionNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /public/forms/{formId}:
    servers:
      - url: /api
        description: Public form runtime API
    get:
      tags:
        - Public Forms
      summary: Get public form configuration
      description: |
        Retrieve the published form definition used by hosted and embedded public forms.
        This response includes the sanitized form structure, theme tokens, telemetry token,
        and optional public bot protection configuration for the form.

        This endpoint is unauthenticated and rate-limited for scraping protection.
      operationId: getPublicForm
      security: []
      parameters:
        - $ref: '#/components/parameters/formId'
      responses:
        '200':
          description: Public form configuration returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicForm'
              example:
                id: "form_abc123"
                title: "Contact Form"
                description: "General contact form for website visitors"
                settings:
                  redirectUrl: "https://example.com/thanks"
                  submitButtonText: "Send message"
                  successMessage: "Thanks for reaching out."
                botProtection:
                  provider: "turnstile"
                  siteKey: "1x00000000000000000000AA"
                  mode: "invisible"
                  offlineQueueEnabled: false
                telemetry:
                  analyticsToken: "eyJhbGciOi..."
                theme:
                  preset: "modern"
                  tokens:
                    colors:
                      primary:
                        base: "#2563eb"
                fields:
                  - id: "fld_email01"
                    type: "email"
                    label: "Email Address"
                    placeholder: "you@example.com"
                    description: "We'll use this to reply"
                    required: true
                    config: {}
                    validationRules: {}
                    order: 0
                    pageId: "page_contact"
                pages:
                  - id: "page_contact"
                    title: "Contact Information"
                    description: null
                    order: 0
                    conditionalLogic: null
                    fields:
                      - id: "fld_email01"
                        type: "email"
                        label: "Email Address"
                        placeholder: "you@example.com"
                        description: "We'll use this to reply"
                        required: true
                        config: {}
                        validationRules: {}
                        order: 0
                        pageId: "page_contact"
        '404':
          $ref: '#/components/responses/PublicFormNotFound'
        '429':
          $ref: '#/components/responses/PublicRateLimited'
        '500':
          $ref: '#/components/responses/PublicInternalError'
    post:
      tags:
        - Public Forms
      summary: Submit a published public form
      description: |
        Submit values to a published public form from the hosted submit page or an embed.
        This endpoint is unauthenticated and uses per-form rate limiting.

        If the form enables public bot protection, the request must include a `captchaToken`
        issued by the public form runtime. Authenticated API submissions should use the
        `/api/v1/forms/{formId}/submissions` endpoint instead.
      operationId: submitPublicForm
      security: []
      parameters:
        - $ref: '#/components/parameters/formId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicSubmissionRequest'
            examples:
              protected_form:
                summary: Public submission with Turnstile token
                value:
                  values:
                    fld_email01: "john@example.com"
                    fld_msg01: "Hello from the public form."
                  submitterEmail: "john@example.com"
                  submitterName: "John Doe"
                  captchaToken: "0.zrSnR6..."
              unprotected_form:
                summary: Public submission without bot protection
                value:
                  values:
                    fld_email01: "user@example.com"
      responses:
        '200':
          description: Submission created successfully.
          headers:
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSubmissionSuccess'
              example:
                success: true
                submissionId: "sub_xyz789"
        '400':
          $ref: '#/components/responses/PublicBadRequest'
        '404':
          $ref: '#/components/responses/PublicFormNotFound'
        '413':
          $ref: '#/components/responses/PublicPayloadTooLarge'
        '422':
          description: |
            Submission rejected because validation failed or the security challenge was not completed.
          headers:
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PublicSubmissionValidationError'
                  - $ref: '#/components/schemas/PublicError'
              examples:
                validation_error:
                  summary: Field validation failed
                  value:
                    success: false
                    errors:
                      fld_email01: "Email Address is required"
                captcha_required:
                  summary: CAPTCHA challenge missing or failed
                  value:
                    success: false
                    error: "Please complete the security check and try again."
        '429':
          $ref: '#/components/responses/PublicRateLimited'
        '503':
          $ref: '#/components/responses/PublicVerificationUnavailable'
        '500':
          $ref: '#/components/responses/PublicInternalError'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Pass your API key as a Bearer token in the Authorization header.

        ```
        Authorization: Bearer bf_your_api_key_here
        ```
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Pass your API key in the X-API-Key custom header.

        ```
        X-API-Key: bf_your_api_key_here
        ```

  parameters:
    formId:
      name: formId
      in: path
      required: true
      description: The unique form identifier (prefixed with `form_`).
      schema:
        type: string
        pattern: '^form_[a-zA-Z0-9]+$'
      example: "form_abc123"

    submissionId:
      name: submissionId
      in: path
      required: true
      description: The unique submission identifier (prefixed with `sub_`).
      schema:
        type: string
        pattern: '^sub_[a-zA-Z0-9]+$'
      example: "sub_xyz789"

  headers:
    X-RateLimit-Limit:
      description: Maximum number of requests allowed per window.
      schema:
        type: integer
      example: 100

    X-RateLimit-Remaining:
      description: Number of requests remaining in the current window.
      schema:
        type: integer
      example: 97

    X-RateLimit-Reset:
      description: Unix timestamp (seconds) when the current rate limit window resets.
      schema:
        type: integer
      example: 1706000000

    Retry-After:
      description: Seconds until the rate limit resets. Only present on 429 responses.
      schema:
        type: integer
      example: 42

  schemas:
    FormSummary:
      type: object
      required:
        - id
        - title
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: Unique form identifier.
          example: "form_abc123"
        title:
          type: string
          description: Form title.
          example: "Contact Form"
        description:
          type: string
          nullable: true
          description: Form description, if set.
          example: "General contact form for website visitors"
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: |
            Current form status.
            - `draft`: Form is being edited and not accepting submissions.
            - `published`: Form is live and accepting submissions.
            - `archived`: Form is no longer active.
          example: "published"
        createdAt:
          type: string
          format: date-time
          description: When the form was created (ISO 8601).
          example: "2025-01-15T10:30:00.000Z"
        updatedAt:
          type: string
          format: date-time
          description: When the form was last updated (ISO 8601).
          example: "2025-01-20T14:45:00.000Z"

    FormDetail:
      description: Full form definition including pages, fields, and settings.
      allOf:
        - $ref: '#/components/schemas/FormSummary'
        - type: object
          required:
            - pages
            - fields
          properties:
            settings:
              type: object
              description: Form-level settings (progress bar, button labels, etc.).
              additionalProperties: true
            pages:
              type: array
              description: Ordered list of form pages.
              items:
                $ref: '#/components/schemas/FormPage'
            fields:
              type: array
              description: Ordered list of field definitions for this form.
              items:
                $ref: '#/components/schemas/FormField'

    FormPage:
      type: object
      required:
        - id
        - title
        - order
      properties:
        id:
          type: string
          description: Unique page identifier. Can be user-provided or auto-generated.
          example: "page_abc123"
        title:
          type: string
          description: Page title shown to users.
          example: "Contact Information"
        description:
          type: string
          nullable: true
          description: Optional page description.
        order:
          type: integer
          description: Display order (0-indexed, ascending).
          example: 0
        conditionalLogic:
          $ref: '#/components/schemas/ConditionalLogic'

    ConditionalLogic:
      type: object
      nullable: true
      description: |
        Show or hide this element based on field values. When `show` is true, the element
        is visible only when the rules match. When false, it's hidden when rules match.
      required:
        - show
        - logic
        - rules
      properties:
        show:
          type: boolean
          description: If true, show when rules match. If false, hide when rules match.
        logic:
          type: string
          enum: [all, any]
          description: Whether all rules must match or any single rule.
        rules:
          type: array
          items:
            type: object
            required:
              - fieldId
              - operator
            properties:
              fieldId:
                type: string
                description: ID of the field to evaluate.
              operator:
                type: string
                enum: [equals, notEquals, contains, notContains, greaterThan, lessThan, greaterThanOrEquals, lessThanOrEquals, isEmpty, isNotEmpty]
              value:
                description: Value to compare against. Type depends on the field.

    FormField:
      type: object
      required:
        - type
        - label
        - required
        - order
        - config
        - validationRules
      properties:
        id:
          type: string
          description: Unique field identifier. Auto-generated if omitted on create.
          example: "fld_email01"
        type:
          type: string
          description: |
            Field input type. Common types: `text`, `email`, `textarea`, `select`, `checkbox`,
            `radio`, `number`, `date`, `phone`, `url`, `file-upload`, `rating`, `scale`,
            `nps`, `likert-score`, `ranking`, `signature`, `heading`.
          example: "email"
        label:
          type: string
          description: Human-readable field label shown to users.
          example: "Email Address"
        placeholder:
          type: string
          nullable: true
          description: Placeholder text shown in empty input fields.
          example: "you@example.com"
        description:
          type: string
          nullable: true
          description: Help text displayed below the field.
        required:
          type: boolean
          description: Whether this field must be filled in to submit the form.
          example: true
        config:
          type: object
          description: |
            Field-type-specific configuration. For radio/select fields, includes `options` array.
            For text fields, may include `maxLength`. Structure varies by field type.
          additionalProperties: true
          example:
            options:
              - label: "Yes"
                value: "yes"
              - label: "No"
                value: "no"
            layout: "horizontal"
        validationRules:
          type: object
          description: Validation constraints (e.g., required, minLength, pattern).
          additionalProperties: true
          example: {}
        order:
          type: integer
          description: Display order within the form (0-indexed, ascending).
          example: 0
        pageId:
          type: string
          nullable: true
          description: ID of the page this field belongs to. References a page ID from the `pages` array.
        conditionalLogic:
          $ref: '#/components/schemas/ConditionalLogic'
        hidden:
          type: boolean
          description: If true, field is hidden from the user (used with prefillParam).
        prefillParam:
          type: string
          description: URL query parameter name to pre-fill this field's value.

    SubmissionSummary:
      type: object
      required:
        - id
        - createdAt
        - data
      properties:
        id:
          type: string
          description: Unique submission identifier.
          example: "sub_xyz789"
        submitterEmail:
          type: string
          nullable: true
          description: Email of the person who submitted, if provided.
          example: "john@example.com"
        submitterName:
          type: string
          nullable: true
          description: Name of the person who submitted, if provided.
          example: "John Doe"
        createdAt:
          type: string
          format: date-time
          description: When the submission was created (ISO 8601).
          example: "2025-01-22T11:30:00.000Z"
        data:
          type: object
          additionalProperties: true
          description: |
            Submitted data as a map of field IDs to values.
            Keys correspond to `FormField.id` values from the form definition.
          example:
            fld_email01: "john@example.com"
            fld_name01: "John Doe"
            fld_msg01: "Hello, I have a question about pricing."

    SubmissionDetail:
      type: object
      required:
        - id
        - formId
        - createdAt
        - data
        - rawData
      properties:
        id:
          type: string
          description: Unique submission identifier.
          example: "sub_xyz789"
        formId:
          type: string
          description: The form this submission belongs to.
          example: "form_abc123"
        formTitle:
          type: string
          description: Title of the form at time of retrieval.
          example: "Contact Form"
        submitterEmail:
          type: string
          nullable: true
          description: Email of the person who submitted, if provided.
          example: "john@example.com"
        submitterName:
          type: string
          nullable: true
          description: Name of the person who submitted, if provided.
          example: "John Doe"
        createdAt:
          type: string
          format: date-time
          description: When the submission was created (ISO 8601).
          example: "2025-01-22T11:30:00.000Z"
        data:
          type: object
          additionalProperties: true
          description: |
            Submitted data with **field labels** as keys. Useful for human-readable display.
            Note: if a field is deleted from the form, the key falls back to the field ID.
          example:
            "Email Address": "john@example.com"
            "Full Name": "John Doe"
            "Message": "Hello, I have a question about pricing."
        rawData:
          type: object
          additionalProperties: true
          description: |
            Submitted data with **field IDs** as keys. Use this for programmatic access
            since field IDs are stable even if labels change.
          example:
            fld_email01: "john@example.com"
            fld_name01: "John Doe"
            fld_msg01: "Hello, I have a question about pricing."

    CreateSubmissionRequest:
      type: object
      required:
        - data
      properties:
        data:
          type: object
          description: |
            Key-value pairs where keys are field IDs (from the form definition) and
            values are the submitted data. All required fields must be included.
          additionalProperties: true
          example:
            fld_email01: "john@example.com"
            fld_name01: "John Doe"
            fld_msg01: "Hello, I would like to learn more about your services."
        submitterEmail:
          type: string
          format: email
          description: Optional email address of the submitter. Stored as metadata.
          example: "john@example.com"
        submitterName:
          type: string
          description: Optional name of the submitter. Stored as metadata.
          example: "John Doe"

    CreatedSubmission:
      type: object
      required:
        - id
        - formId
        - createdAt
      properties:
        id:
          type: string
          description: The unique identifier for the newly created submission.
          example: "sub_abc123xyz"
        formId:
          type: string
          description: The form this submission was created for.
          example: "form_abc123"
        createdAt:
          type: string
          format: date-time
          description: When the submission was created (ISO 8601).
          example: "2025-01-22T15:30:00.000Z"

    PublicFormSettings:
      type: object
      required:
        - submitButtonText
        - successMessage
      properties:
        redirectUrl:
          type: string
          nullable: true
          description: Optional redirect URL configured for the public form success state.
          example: "https://example.com/thanks"
        submitButtonText:
          type: string
          description: Submit button label shown in the public renderer.
          example: "Submit"
        successMessage:
          type: string
          description: Success message shown after public submission when no redirect applies.
          example: "Thank you for your submission!"

    PublicBotProtectionConfig:
      type: object
      required:
        - provider
        - siteKey
        - mode
        - offlineQueueEnabled
      properties:
        provider:
          type: string
          enum: [turnstile]
        siteKey:
          type: string
          description: Public Turnstile site key for the form.
          example: "1x00000000000000000000AA"
        mode:
          type: string
          enum: [invisible]
        offlineQueueEnabled:
          type: boolean
          description: Whether offline queueing is allowed for protected public forms.
          enum: [false]
          example: false

    PublicTelemetryConfig:
      type: object
      required:
        - analyticsToken
      properties:
        analyticsToken:
          type: string
          description: Signed token used by the hosted/embed runtime for public analytics events.
          example: "eyJhbGciOi..."

    PublicTheme:
      type: object
      nullable: true
      required:
        - preset
        - tokens
      properties:
        preset:
          type: string
          nullable: true
          description: Name of the theme preset applied to the public form, if any.
          example: "modern"
        tokens:
          type: object
          description: Theme token bundle used by the public renderer.
          additionalProperties: true

    PublicFormPage:
      allOf:
        - $ref: '#/components/schemas/FormPage'
        - type: object
          required:
            - fields
          properties:
            fields:
              type: array
              items:
                $ref: '#/components/schemas/FormField'

    PublicForm:
      type: object
      required:
        - id
        - title
        - settings
        - botProtection
        - telemetry
        - theme
        - fields
        - pages
      properties:
        id:
          type: string
          description: Published form identifier used by hosted and embedded public forms.
          example: "form_abc123"
        title:
          type: string
          example: "Contact Form"
        description:
          type: string
          nullable: true
          example: "General contact form for website visitors"
        settings:
          $ref: '#/components/schemas/PublicFormSettings'
        botProtection:
          allOf:
            - oneOf:
                - $ref: '#/components/schemas/PublicBotProtectionConfig'
                - type: 'null'
          description: Public bot protection config. `null` means no CAPTCHA challenge is required.
        telemetry:
          $ref: '#/components/schemas/PublicTelemetryConfig'
        theme:
          $ref: '#/components/schemas/PublicTheme'
        fields:
          type: array
          items:
            $ref: '#/components/schemas/FormField'
        pages:
          type: array
          items:
            $ref: '#/components/schemas/PublicFormPage'

    PublicSubmissionRequest:
      type: object
      required:
        - values
      properties:
        values:
          type: object
          description: |
            Key-value pairs where keys are public form field IDs and values are the submitted input.
          additionalProperties: true
          example:
            fld_email01: "john@example.com"
            fld_msg01: "Hello from the public form."
        submitterEmail:
          type: string
          format: email
          description: Optional submitter email metadata.
          example: "john@example.com"
        submitterName:
          type: string
          description: Optional submitter name metadata.
          example: "John Doe"
        captchaToken:
          type: string
          description: |
            Required when `botProtection` is enabled on the public form. This token must come from
            the hosted or embedded public runtime challenge flow.
          minLength: 8
          maxLength: 4096
          example: "0.zrSnR6..."

    PublicSubmissionSuccess:
      type: object
      required:
        - success
        - submissionId
      properties:
        success:
          type: boolean
          enum: [true]
        submissionId:
          type: string
          example: "sub_xyz789"

    PublicSubmissionValidationError:
      type: object
      required:
        - success
        - errors
      properties:
        success:
          type: boolean
          enum: [false]
        errors:
          type: object
          additionalProperties:
            type: string
          example:
            fld_email01: "Email Address is required"

    PublicError:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          type: string
          example: "Form not found or not published"

    Pagination:
      type: object
      required:
        - limit
        - offset
        - total
      properties:
        limit:
          type: integer
          description: Maximum items returned per page.
          example: 50
        offset:
          type: integer
          description: Number of items skipped.
          example: 0
        total:
          type: integer
          description: Total number of items across all pages.
          example: 127

    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: |
            Machine-readable error code. Common values:
            - `unauthorized` - Missing or invalid API key
            - `forbidden` - API key lacks required scope
            - `not_found` - Resource does not exist
            - `rate_limit_exceeded` - Too many requests
            - `invalid_json` - Request body is not valid JSON
            - `invalid_data` - Request body structure is invalid
            - `missing_required_fields` - Required form fields not provided
            - `form_not_published` - Form is not in published status
            - `limit_exceeded` - Monthly submission quota reached
            - `internal_error` - Unexpected server error
          example: "not_found"
        message:
          type: string
          description: Human-readable error description with additional context.
          example: "Form not found"

  responses:
    Unauthorized:
      description: |
        Authentication required. No API key was provided, or the provided key is
        invalid or expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing_key:
              summary: No API key provided
              value:
                error: "unauthorized"
                message: "API key required. Provide via Authorization: Bearer <key> or X-API-Key header."
            invalid_key:
              summary: Invalid or expired API key
              value:
                error: "unauthorized"
                message: "Invalid or expired API key."

    ForbiddenFormsRead:
      description: API key lacks the `forms:read` scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "forbidden"
            message: "Insufficient permissions. Required scope: forms:read"

    ForbiddenSubmissionsRead:
      description: API key lacks the `submissions:read` scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "forbidden"
            message: "Insufficient permissions. Required scope: submissions:read"

    ForbiddenSubmissionsWrite:
      description: API key lacks the `submissions:write` scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "forbidden"
            message: "Insufficient permissions. Required scope: submissions:write"

    FormNotFound:
      description: The specified form does not exist or does not belong to your organization.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "not_found"
            message: "Form not found"

    SubmissionNotFound:
      description: The specified submission does not exist or does not belong to your organization.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "not_found"
            message: "Submission not found"

    RateLimited:
      description: |
        Too many requests. You have exceeded the rate limit for your plan.
        Check the `Retry-After` header for when to retry.
      headers:
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          description: Will be 0 when rate limited.
          schema:
            type: integer
          example: 0
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "rate_limit_exceeded"
            message: "Rate limit exceeded. Retry after 42 seconds."

    InternalError:
      description: An unexpected server error occurred. If this persists, contact support.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "internal_error"
            message: "An internal error occurred"

    PublicBadRequest:
      description: Public request body or path data is invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
          examples:
            invalid_json:
              summary: Invalid JSON body
              value:
                success: false
                error: "Invalid JSON body"
            invalid_submission:
              summary: Invalid submission payload
              value:
                success: false
                error: "Invalid submission data"

    PublicFormNotFound:
      description: The form does not exist or is not published for public access.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
          example:
            success: false
            error: "Form not found or not published"

    PublicPayloadTooLarge:
      description: Public submission payload exceeded the 1MB limit.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
          example:
            success: false
            error: "Request body exceeds 1MB limit"

    PublicRateLimited:
      description: Too many public requests or submissions. Retry after the indicated delay.
      headers:
        X-RateLimit-Remaining:
          description: Requests or submissions remaining in the current window.
          schema:
            type: integer
          example: 0
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
          example:
            success: false
            error: "Too many submissions. Please try again later."

    PublicVerificationUnavailable:
      description: Public bot verification could not be completed because the verification backend was unavailable.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
          example:
            success: false
            error: "Security verification is temporarily unavailable. Please try again."

    PublicInternalError:
      description: An unexpected public-form server error occurred.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
          example:
            success: false
            error: "Internal server error"
