openapi: 3.1.0
info:
  title: quillflow protocol
  version: 1.0.0
  description: |
    What a portal's backend exposes so `<qf-form>` can load a form, look up data and submit.
    Implemented by `@quillflow/server-node`. The .NET SDK implements the same contract.

    * Data sources are **named, not addressed**: the browser sends only the parameters the form
      declares, and the server runs the matching connector with the caller's identity.
    * Submissions are **re-validated** with the same engine and rules as the browser. Only the
      cleaned data (visible fields, recomputed calculations) reaches the submission handler.
    * Errors use RFC 9457 problem details (`application/problem+json`).
    * Mount the API under any prefix (e.g. `/api`); the element's `api` attribute points to it.
paths:
  /forms/{formId}:
    get:
      summary: Latest published version of a form
      parameters: [{ $ref: "#/components/parameters/formId" }]
      responses:
        "200":
          description: The canonical definition. `Cache-Control: no-cache`.
          content:
            application/json:
              schema: { $ref: "./form-definition.schema.json" }
        "404": { $ref: "#/components/responses/Problem" }
  /forms/{formId}/versions/{version}:
    get:
      summary: A specific, immutable version of a form
      parameters: [{ $ref: "#/components/parameters/formId" }, { $ref: "#/components/parameters/version" }]
      responses:
        "200":
          description: The canonical definition. `Cache-Control: public, max-age=31536000, immutable`.
          content:
            application/json:
              schema: { $ref: "./form-definition.schema.json" }
        "404": { $ref: "#/components/responses/Problem" }
  /forms/{formId}/versions/{version}/datasources/{dataSource}:
    post:
      summary: Run a data source declared by the form
      parameters:
        - { $ref: "#/components/parameters/formId" }
        - { $ref: "#/components/parameters/version" }
        - { name: dataSource, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [params]
              additionalProperties: false
              properties:
                params:
                  description: |
                    Exactly the parameter names the data source declares. Values are text (max 1000 characters),
                    numbers or booleans. A search lookup (`query: true`) also takes `query`: what the user typed
                    into a search field, as text of 1–200 characters.
                  type: object
                  additionalProperties:
                    oneOf: [{ type: string, maxLength: 1000 }, { type: number }, { type: boolean }]
      responses:
        "200":
          description: The connector's result. `Cache-Control: no-store` (it may contain personal data).
          content:
            application/json:
              schema:
                type: object
                required: [result]
                properties:
                  result: { description: Any JSON value; null when nothing was found. }
        "400": { $ref: "#/components/responses/Problem" }
        "401": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
        "502": { $ref: "#/components/responses/Problem" }
  /forms/{formId}/versions/{version}/submissions:
    post:
      summary: Submit a completed form
      parameters:
        - { $ref: "#/components/parameters/formId" }
        - { $ref: "#/components/parameters/version" }
        - name: Idempotency-Key
          in: header
          description: |
            Chosen by the client once per form instance. A repeat with the same key and data
            returns the original receipt (header `Idempotent-Replayed: true`). The same key with
            different data gives 422.
          schema: { type: string, pattern: "^[\\w.:-]{8,128}$" }
        - name: Accept-Language
          in: header
          description: Language of validation messages; falls back to the form's default language.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data:
                  description: Nested form data (groups as objects). Unknown keys are ignored.
                  type: object
      responses:
        "201":
          description: Accepted and handed to the submission handler.
          content:
            application/json:
              schema:
                type: object
                required: [reference, receivedAt, formId, version]
                properties:
                  reference: { type: string, description: Shown to the applicant (e.g. a case number). }
                  receivedAt: { type: string, format: date-time }
                  formId: { type: string }
                  version: { type: string }
        "400": { $ref: "#/components/responses/Problem" }
        "413": { $ref: "#/components/responses/Problem" }
        "415": { $ref: "#/components/responses/Problem" }
        "422":
          description: The data breaks the form's rules, or the Idempotency-Key was reused with different data.
          content:
            application/problem+json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/Problem" }
                  - type: object
                    properties:
                      errors:
                        type: array
                        items:
                          type: object
                          required: [path, code, message]
                          properties:
                            path: { type: string, example: loan.amount }
                            code: { enum: [required, type, choice, constraint] }
                            message: { type: string }
components:
  parameters:
    formId: { name: formId, in: path, required: true, schema: { type: string } }
    version: { name: version, in: path, required: true, schema: { type: string } }
  responses:
    Problem:
      description: RFC 9457 problem details
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
  schemas:
    Problem:
      type: object
      required: [status, title]
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
