openapi: 3.0.0
info:
  title: Paubox Forms API
  description: >
    The Paubox Forms API has two kinds of endpoints. Public, respondent-facing
    endpoints (retrieving a form definition for rendering and submitting a
    form response) require no authentication. Authenticated management
    endpoints (creating, listing, updating, copying, archiving forms, and
    reading or exporting submissions) require a Paubox API key with the
    "forms" scope, sent as `Authorization: Bearer YOUR_API_KEY`. API keys are
    generated in the Paubox dashboard.
  version: 1.0.0
servers:
  - url: https://api.paubox.com/v1/forms
    description: Paubox Forms API
tags:
  - name: Forms
    description: Retrieve form definitions and accept submissions
  - name: Form management
    description: Create, list, update, copy, and archive forms (requires API key)
  - name: Submissions
    description: List and export form submissions (requires API key)
paths:
  /public/form_data/{form_id}:
    get:
      tags: [Forms]
      summary: Get form metadata
      operationId: getPublicForm
      security: []
      description: >
        Returns the full form definition (HTML, JSON schema, CSS) for a given
        form. Called by the form embed before rendering a form to a respondent.
        No authentication required.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form to retrieve
      responses:
        "200":
          description: Form found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Form'
              examples:
                example:
                  summary: Example response
                  value:
                    id: "550e8400-e29b-41d4-a716-446655440000"
                    title: "Patient Intake Form"
                    description: "Please complete before your appointment."
                    form_json: {}
                    form_html: "<form>...</form>"
                    form_css: "form { font-family: sans-serif; }"
                    active: true
                    customer_id: 123
                    signable: false
                    submission_count: 42
                    created_at: "2024-01-15T10:30:00Z"
                    updated_at: "2024-06-01T08:00:00Z"
        "404":
          description: Form not found

  /api/forms:
    get:
      tags: [Form management]
      summary: List forms
      operationId: listForms
      security:
        - bearerAuth: []
      description: >
        Returns a paginated list of forms belonging to a customer. Supports
        filtering by form ID, title/description search, and archived/active
        state, plus ordering and pagination.
      parameters:
        - name: customer_id
          in: query
          required: true
          schema:
            type: integer
          description: >
            Your Paubox customer ID. Requests for a customer you do not have
            access to return 403.
        - name: form_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter the results to a single form by its UUID.
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: >
            Substring match against the form title or description.
        - name: archived
          in: query
          required: false
          schema:
            type: boolean
          description: Filter by archived state.
        - name: active
          in: query
          required: false
          schema:
            type: boolean
          description: Filter by active state.
        - name: order_by
          in: query
          required: false
          schema:
            type: string
            enum: [created_at, title, updated_at, submission_count]
            default: created_at
          description: >
            Field to order results by. Unknown values fall back to
            `created_at`.
        - name: order
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: desc
          description: Sort direction.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: Page number to return.
        - name: items
          in: query
          required: false
          schema:
            type: integer
            default: 50
            maximum: 100
          description: Number of forms per page (maximum 100).
      responses:
        "200":
          description: Paginated list of forms
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormListResponse'
              examples:
                example:
                  summary: Example response
                  value:
                    results:
                      - id: "550e8400-e29b-41d4-a716-446655440000"
                        title: "Patient Intake Form"
                        description: "Please complete before your appointment."
                        form_html: "<form>...</form>"
                        form_json: {}
                        form_css: "form { font-family: sans-serif; }"
                        vanity_url: null
                        version: 2
                        active: true
                        customer_id: 123
                        old_form_id: null
                        recipient: "intake@example.com,records@example.com"
                        signable: false
                        signature_confirmation_label: null
                        submission_count: 42
                        type: null
                        subscription_list_id: null
                        deleted: false
                        archived: false
                        created_at: "2024-01-15T10:30:00Z"
                        updated_at: "2024-06-01T08:00:00Z"
                    page_info:
                      count: 1
                      pages: 1
                      page: 1
                      items: 50
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "403":
          description: The API key does not have access to the requested customer
    post:
      tags: [Form management]
      summary: Create a form
      operationId: createForm
      security:
        - bearerAuth: []
      description: >
        Creates a new form. Returns the UUID of the created form.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateFormRequest'
            examples:
              basic:
                summary: Minimal form
                value:
                  title: "Patient Intake Form"
                  form_json:
                    body:
                      - type: "TextInput"
                        id: "pi-01"
                        properties:
                          field_name: "first_name"
                          text: "<p>First name</p>"
                          required: true
                          input_mask: "Any"
                      - type: "Button"
                        id: "pi-02"
                        properties:
                          margin: "4px"
                          full_width: true
                  customer_id: 123
                  version: 1
              full:
                summary: Marketing form with notifications
                value:
                  title: "Newsletter Signup"
                  description: "Sign up for our monthly newsletter."
                  form_json:
                    body:
                      - type: "TextInput"
                        id: "nl-01"
                        properties:
                          field_name: "email"
                          text: "<p>Email</p>"
                          required: true
                          input_mask: "Email"
                      - type: "Button"
                        id: "nl-02"
                        properties:
                          margin: "4px"
                  form_html: "<form>...</form>"
                  form_css: "form { font-family: sans-serif; }"
                  customer_id: 123
                  version: 1
                  recipient: "marketing@example.com"
                  subscription_list_id: "b3f9c2d1-4a5e-4f6b-8c7d-9e0f1a2b3c4d"
                  type: "marketing_form"
                  active: true
      responses:
        "200":
          description: Form created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                    description: UUID of the new form
              examples:
                example:
                  summary: Example response
                  value:
                    id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "403":
          description: The API key does not have access to the requested customer
        "422":
          description: >
            `form_json` is not a schema the form renderer can draw. The message
            names the offending component's index and type, for example:
            "form_json.body[1].type `DatePicker` is not a component the form
            renderer knows". A missing `body` array reports "form_json must have
            a `body` array of components".
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormJsonValidationError'
              examples:
                fields_shape:
                  summary: Legacy `fields` payload
                  value:
                    message: >-
                      form_json must have a `body` array of components; a form
                      without one renders as an empty page

  /api/forms/stats:
    get:
      tags: [Form management]
      summary: Get form statistics
      operationId: getFormStats
      security:
        - bearerAuth: []
      description: >
        Returns aggregate form statistics for a customer: the number of active
        forms, the total submission count, and the number of submissions
        received in the last 7 days.
      parameters:
        - name: customer_id
          in: query
          required: false
          schema:
            type: integer
          description: >
            Paubox customer ID. Defaults to the customer that owns the API
            key.
      responses:
        "200":
          description: Form statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormStats'
              examples:
                example:
                  summary: Example response
                  value:
                    active_form_count: 8
                    total_submission_count: 1204
                    submissions_last_7_days: 37
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "403":
          description: The API key does not have access to the requested customer

  /api/forms/copy:
    post:
      tags: [Form management]
      summary: Copy a form
      operationId: copyForm
      security:
        - bearerAuth: []
      description: >
        Creates a copy of an existing form with a new title. The copy starts
        with a submission count of 0 and no vanity URL. Returns the full new
        form object.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CopyFormRequest'
            examples:
              example:
                summary: Copy a form
                value:
                  form_id: "550e8400-e29b-41d4-a716-446655440000"
                  title: "Patient Intake Form (Copy)"
      responses:
        "200":
          description: The newly created form
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Form'
              examples:
                example:
                  summary: Example response
                  value:
                    id: "9f8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d"
                    title: "Patient Intake Form (Copy)"
                    description: "Please complete before your appointment."
                    form_html: "<form>...</form>"
                    form_json: {}
                    form_css: "form { font-family: sans-serif; }"
                    vanity_url: null
                    version: 2
                    active: false
                    customer_id: 123
                    old_form_id: null
                    recipient: "intake@example.com"
                    signable: false
                    signature_confirmation_label: null
                    submission_count: 0
                    type: null
                    subscription_list_id: null
                    deleted: false
                    archived: false
                    created_at: "2024-06-10T12:00:00Z"
                    updated_at: "2024-06-10T12:00:00Z"
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "403":
          description: The form belongs to a different customer
        "422":
          description: >
            The **source** form's stored `form_json` is not a schema the renderer
            can draw, so copying it would produce a second unrenderable form.
            Note this can be returned for input the caller never sent: the
            rejected schema belongs to the form being copied, not to the request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormJsonValidationError'
        "404":
          description: Source form not found

  /api/forms/{form_id}:
    get:
      tags: [Form management]
      summary: Get a form
      operationId: getForm
      security:
        - bearerAuth: []
      description: >
        Returns the full form definition by ID, including inactive and
        archived forms (unlike the public endpoint, which only serves
        renderable forms to respondents).
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form to retrieve
      responses:
        "200":
          description: Form found
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Form'
              examples:
                example:
                  summary: Example response
                  value:
                    data:
                      id: "550e8400-e29b-41d4-a716-446655440000"
                      title: "Patient Intake Form"
                      description: "Please complete before your appointment."
                      form_html: "<form>...</form>"
                      form_json: {}
                      form_css: "form { font-family: sans-serif; }"
                      vanity_url: null
                      version: 2
                      active: true
                      customer_id: 123
                      old_form_id: null
                      recipient: "intake@example.com"
                      signable: false
                      signature_confirmation_label: null
                      submission_count: 42
                      type: null
                      subscription_list_id: null
                      deleted: false
                      archived: false
                      created_at: "2024-01-15T10:30:00Z"
                      updated_at: "2024-06-01T08:00:00Z"
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "403":
          description: The form belongs to a different customer
        "500":
          description: >
            Form not found. The service currently returns 500 (rather than
            404) for a form ID that does not exist.
    put:
      tags: [Form management]
      summary: Update a form
      operationId: updateForm
      security:
        - bearerAuth: []
      description: >
        Updates a form. This is a partial update: any fields omitted from the
        request body are left unchanged.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form to update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateFormRequest'
            examples:
              example:
                summary: Rename and activate a form
                value:
                  title: "Patient Intake Form (2024)"
                  active: true
      responses:
        "200":
          description: Form updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
                  form_id:
                    type: string
                    format: uuid
              examples:
                example:
                  summary: Example response
                  value:
                    detail: "Form updated successfully"
                    form_id: "550e8400-e29b-41d4-a716-446655440000"
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "403":
          description: The form belongs to a different customer
        "422":
          description: >
            `form_json` is not a schema the form renderer can draw. Only checked
            when the payload includes `form_json` — a partial update that omits
            it (changing `title` or `active`, say) is never validated and leaves
            the stored schema untouched. A rejected update applies nothing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormJsonValidationError'
        "404":
          description: Form not found or deleted

  /api/forms/{form_id}/archive:
    post:
      tags: [Form management]
      summary: Archive a form
      operationId: archiveForm
      security:
        - bearerAuth: []
      description: >
        Archives a form. Archiving also deactivates the form (sets `active`
        to false), so it stops accepting submissions. The endpoint does not
        verify that the form exists: an unknown form ID still returns a 200
        success response.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form to archive
      responses:
        "200":
          description: Form archived
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
              examples:
                example:
                  summary: Example response
                  value:
                    detail: "Form archived."
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope

  /api/forms/{form_id}/unarchive:
    post:
      tags: [Form management]
      summary: Unarchive a form
      operationId: unarchiveForm
      security:
        - bearerAuth: []
      description: >
        Unarchives a form. This does not re-activate it: `active` stays false
        until the form is updated with `active: true`. The endpoint does not
        verify that the form exists: an unknown form ID still returns a 200
        success response.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form to unarchive
      responses:
        "200":
          description: Form unarchived
          content:
            application/json:
              schema:
                type: object
                properties:
                  detail:
                    type: string
              examples:
                example:
                  summary: Example response
                  value:
                    detail: "Form unarchived."
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope

  /api/forms/{form_id}/submissions:
    post:
      tags: [Forms]
      summary: Submit a form response
      operationId: createFormSubmission
      security: []
      description: >
        Submits a respondent's answers for a form. No authentication required.
        On success, the service stores the submission, increments the form's
        submission count, emails recipients (if configured), and returns 201
        with no body.


        Maximum request size is **250 MB** (to support file attachments).
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form being submitted
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormSubmissionRequest'
            examples:
              basic:
                summary: Text fields only
                value:
                  form_data:
                    first_name: "Jane"
                    last_name: "Smith"
                    email: "jane@example.com"
              with_attachments:
                summary: With file attachments
                value:
                  form_data:
                    first_name: "Jane"
                    signature: "{signature_field}"
                  attachments:
                    - name: "consent.pdf"
                      content: "JVBERi0xLjQ..."
      responses:
        "201":
          description: Submission accepted
        "400":
          description: Missing required `form_data` field
        "500":
          description: >
            Form not found. The service currently returns 500 (rather than
            404) when submitting to a form ID that does not exist.
    get:
      tags: [Submissions]
      summary: List form submissions
      operationId: listFormSubmissions
      security:
        - bearerAuth: []
      description: >
        Returns a paginated list of submissions for a form. Each submission's
        `form_data` field is a JSON-encoded string of the respondent's
        answers.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form
        - name: submission_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
          description: Filter the results to a single submission by its UUID.
        - name: order_by
          in: query
          required: false
          schema:
            type: string
            enum: [created_at, submitter_email]
            default: created_at
          description: Field to order results by.
        - name: order
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: desc
          description: Sort direction.
        - name: page
          in: query
          required: false
          schema:
            type: integer
            default: 1
          description: Page number to return.
        - name: items
          in: query
          required: false
          schema:
            type: integer
            default: 50
            maximum: 100
          description: Number of submissions per page (maximum 100).
      responses:
        "200":
          description: Paginated list of submissions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FormSubmissionListResponse'
              examples:
                example:
                  summary: Example response
                  value:
                    data:
                      - id: "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                        form_id: "550e8400-e29b-41d4-a716-446655440000"
                        form_data: "{\"first_name\":\"Jane\",\"last_name\":\"Smith\",\"email\":\"jane@example.com\"}"
                        storage_type: "S3"
                        storage_url: null
                        submitter_email: "jane@example.com"
                        recipients: "intake@example.com"
                        attachment_name: null
                        attachment_url: null
                        attachment_type: null
                        attachment: null
                        created_at: "2024-06-05T14:22:00Z"
                    total: 42
                    page: 1
                    items: 50
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "403":
          description: The form belongs to a different customer
        "404":
          description: Form not found

  /api/forms/{form_id}/submissions/submission-csv:
    get:
      tags: [Submissions]
      summary: Export submissions as CSV
      operationId: exportSubmissionsCsv
      security:
        - bearerAuth: []
      description: >
        Exports all submissions of a form as a CSV attachment. The
        `Content-Disposition` filename is derived from the form's title plus the
        export date in Pacific time, e.g.
        `New-Patient-Intake-submissions-2026-08-27.csv`. The first column is
        "Created At", followed by one column per form field, using the field
        labels from the form definition.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form
      responses:
        "200":
          description: CSV file with all submissions
          content:
            text/csv:
              schema:
                type: string
              examples:
                example:
                  summary: Example CSV
                  value: |
                    Created At,First name,Last name,Email
                    2024-06-05 02:22:00 PM,Jane,Smith,jane@example.com
                    2024-06-04 09:10:00 AM,John,Doe,john@example.com
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "404":
          description: Form not found

  /api/forms/{form_id}/submissions/submission-csv/{submission_id}:
    get:
      tags: [Submissions]
      summary: Export a submission as CSV
      operationId: exportSubmissionCsv
      security:
        - bearerAuth: []
      description: >
        Exports a single submission as a CSV attachment. The
        `Content-Disposition` filename is derived from the form's title plus the
        submission date in Pacific time, e.g.
        `New-Patient-Intake-2026-08-27.csv`. Uses the same column layout as the
        full export (a "Created At" column followed by one column per form
        field), with a single data row.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form
        - name: submission_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the submission to export
      responses:
        "200":
          description: CSV file with the single submission
          content:
            text/csv:
              schema:
                type: string
              examples:
                example:
                  summary: Example CSV
                  value: |
                    Created At,First name,Last name,Email
                    2024-06-05 02:22:00 PM,Jane,Smith,jane@example.com
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "404":
          description: Form or submission not found

  /api/forms/{form_id}/submissions/{submission_id}/submission-pdf:
    get:
      tags: [Submissions]
      summary: Export a submission as PDF
      operationId: exportSubmissionPdf
      security:
        - bearerAuth: []
      description: >
        Exports a single submission as a PDF attachment. The `Content-Disposition`
        filename is derived from the form's title plus the submission date in
        Pacific time, e.g. `New-Patient-Intake-2026-08-27.pdf`; titles are
        reduced to alphanumerics, `-` and `_`. For signable forms, the PDF
        includes the respondent's signature images.
      parameters:
        - name: form_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the form
        - name: submission_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the submission to export
      responses:
        "200":
          description: PDF file with the submission
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          description: Missing or invalid API key, or the key lacks the "forms" scope
        "500":
          description: >
            Form or submission not found. The service currently returns 500
            (rather than 404) when either the form or the submission does
            not exist.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        Paubox API key sent as `Authorization: Bearer YOUR_API_KEY`. API keys
        are created in the Paubox dashboard and must have the "forms" scope;
        a key without the forms scope receives 401 Unauthorized. A valid key
        used against a resource that belongs to a different customer receives
        403 Forbidden.

  schemas:
    Form:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
          nullable: true
        form_html:
          type: string
          nullable: true
        form_json:
          type: object
          nullable: true
        form_css:
          type: string
          nullable: true
        vanity_url:
          type: string
          nullable: true
        version:
          type: integer
        active:
          type: boolean
        customer_id:
          type: integer
        old_form_id:
          type: integer
          nullable: true
        recipient:
          type: string
          nullable: true
          description: >
            Comma-separated email addresses notified on each submission.
        signable:
          type: boolean
        signature_confirmation_label:
          type: string
          nullable: true
        submission_count:
          type: integer
        type:
          type: string
          nullable: true
        subscription_list_id:
          type: string
          nullable: true
          description: >
            ID of the connected Marketing contact list. For marketing forms,
            new subscribers are added to this list.
        deleted:
          type: boolean
        archived:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    FormSubmission:
      type: object
      properties:
        id:
          type: string
          format: uuid
        form_id:
          type: string
          format: uuid
        form_data:
          type: string
          description: >
            JSON-encoded object of the respondent's answers.
        storage_type:
          type: string
        storage_url:
          type: string
          nullable: true
        submitter_email:
          type: string
          nullable: true
        recipients:
          type: string
          nullable: true
        attachment_name:
          type: string
          nullable: true
        attachment_url:
          type: string
          nullable: true
        attachment_type:
          type: string
          nullable: true
        attachment:
          nullable: true
        created_at:
          type: string
          format: date-time

    PageInfo:
      type: object
      properties:
        count:
          type: integer
          description: Total number of matching forms
        pages:
          type: integer
          description: Total number of pages
        page:
          type: integer
          description: Current page number
        items:
          type: integer
          description: Number of items per page

    FormListResponse:
      type: object
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/Form'
        page_info:
          $ref: '#/components/schemas/PageInfo'

    FormSubmissionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/FormSubmission'
        total:
          type: integer
          description: Total number of matching submissions
        page:
          type: integer
          description: Current page number
        items:
          type: integer
          description: Number of items per page

    FormStats:
      type: object
      properties:
        active_form_count:
          type: integer
          description: Number of active forms
        total_submission_count:
          type: integer
          description: Total submissions across all forms
        submissions_last_7_days:
          type: integer
          description: Submissions received in the last 7 days

    CreateFormRequest:
      type: object
      required: [title, form_json, customer_id, version]
      properties:
        title:
          type: string
          description: Title of the form
        form_json:
          type: object
          description: >
            The form's schema. Must be an object containing a `body` array of
            components; the renderer reads `body` and ignores every other
            top-level key. A payload shaped any other way — notably a `fields`
            array — is rejected with 422. Empty `body` is allowed: a draft
            legitimately has no components yet.
          properties:
            body:
              type: array
              description: >
                Ordered list of components. Each entry is an object with a
                string `type` drawn from the list below, an `id`, and an
                optional `properties` object.
              items:
                type: object
                required: [type]
                properties:
                  type:
                    type: string
                    description: >
                      Component type. Case-sensitive — note `Radiobutton` has a
                      lowercase `b`, unlike the others.
                    enum: [Text, Divider, TextInput, TextArea, Dropdown, Checkboxes,
                           Radiobutton, FileUpload, Signature, Button, Conditional, Logo]
                  id:
                    type: string
                    description: Identifier for the component, unique within the form.
                  properties:
                    type: object
                    description: >
                      Per-component settings such as `field_name`, `text`,
                      `required` and `input_mask`. Not validated on write.
          required: [body]
        customer_id:
          type: integer
          description: Your Paubox customer ID
        version:
          type: integer
          description: Form schema version
        description:
          type: string
        form_html:
          type: string
        form_css:
          type: string
        recipient:
          type: string
          description: >
            Comma-separated email addresses notified on each submission.
        signable:
          type: boolean
          default: false
          description: Whether the form collects a signature
        signature_confirmation_label:
          type: string
        subscription_list_id:
          type: string
          description: >
            ID of the connected Marketing contact list. For marketing forms,
            new subscribers are added to this list.
        type:
          type: string
          description: Form type, for example `marketing_form`
        active:
          type: boolean
          default: false
        submission_count:
          type: integer
          default: 0

    UpdateFormRequest:
      type: object
      description: >
        Partial update. All fields are optional; omitted fields are left
        unchanged.
      properties:
        title:
          type: string
        description:
          type: string
        form_json:
          type: object
          description: >
            The form's schema. Must be an object containing a `body` array of
            components; the renderer reads `body` and ignores every other
            top-level key. A payload shaped any other way — notably a `fields`
            array — is rejected with 422. Empty `body` is allowed: a draft
            legitimately has no components yet.
          properties:
            body:
              type: array
              description: >
                Ordered list of components. Each entry is an object with a
                string `type` drawn from the list below, an `id`, and an
                optional `properties` object.
              items:
                type: object
                required: [type]
                properties:
                  type:
                    type: string
                    description: >
                      Component type. Case-sensitive — note `Radiobutton` has a
                      lowercase `b`, unlike the others.
                    enum: [Text, Divider, TextInput, TextArea, Dropdown, Checkboxes,
                           Radiobutton, FileUpload, Signature, Button, Conditional, Logo]
                  id:
                    type: string
                    description: Identifier for the component, unique within the form.
                  properties:
                    type: object
                    description: >
                      Per-component settings such as `field_name`, `text`,
                      `required` and `input_mask`. Not validated on write.
          required: [body]
        vanity_url:
          type: string
        recipient:
          type: string
          description: >
            Comma-separated email addresses notified on each submission.
        active:
          type: boolean
        subscription_list_id:
          type: string
          description: >
            ID of the connected Marketing contact list. For marketing forms,
            new subscribers are added to this list.

    CopyFormRequest:
      type: object
      required: [form_id, title]
      properties:
        form_id:
          type: string
          format: uuid
          description: UUID of the form to copy
        title:
          type: string
          description: Title for the copy

    FormSubmissionRequest:
      type: object
      required: [form_data]
      properties:
        form_data:
          type: object
          description: >
            Key-value pairs matching the form's field schema (`form_json`).
            Structure varies per form.
        attachments:
          type: array
          description: Optional file attachments
          items:
            type: object
            required: [name, content]
            properties:
              name:
                type: string
                description: Filename
              content:
                type: string
                description: Base64-encoded file content
    FormJsonValidationError:
      type: object
      description: >
        Returned with 422 when `form_json` is not a schema the form renderer can
        draw. Introduced in pb_rforms v1.11.0; before that release these payloads
        were accepted and the resulting form rendered as an empty page.
      properties:
        message:
          type: string
          description: >
            Human-readable reason, naming the offending component's index and
            type where applicable.
      example:
        message: >-
          form_json.body[1].type `DatePicker` is not a component the form
          renderer knows; expected one of: Text, Divider, TextInput, TextArea,
          Dropdown, Checkboxes, Radiobutton, FileUpload, Signature, Button,
          Conditional, Logo
