openapi: 3.0.0
info:
  version: "1.0.0"
  title: Qomon Contact Import API
  description: |
    This private API lets you import contacts from a CSV file into Qomon.

    Authenticate each request with `Authorization: Bearer <API key>`.

    Import flow:
    1. Upload the CSV file with POST /imports.
    2. Configure CSV parsing with POST /imports/{importId}/csv-settings.
    3. Optionally inspect the file with GET /imports/{importId}/columns and GET /imports/{importId}/file-preview.
    4. Retrieve the supported contact fields with GET /imports/{importId}/properties.
    5. Save the zero-based column mapping with POST /imports/{importId}/mapping.
    6. Start processing with POST /imports/{importId}/enqueue.
    7. Poll GET /imports/{importId}/statuses until processing completes or fails.

    All CSV column and row indexes are zero-based.

servers:
  - url: https://incoming.qomon.app
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Imports
    description: Import contacts from CSV files

paths:
  /imports:
    get:
      tags: [Imports]
      operationId: listImports
      summary: List imports
      description: Retrieve imports for the authenticated account.
      responses:
        "200":
          description: Imports retrieved
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Import"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags: [Imports]
      operationId: createImport
      summary: Upload a CSV file
      description: Create an import from a CSV file. The public gateway accepts files up to 10 MB.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV file to import.
      responses:
        "201":
          description: Import created. CSV settings and mapping must be configured before enqueueing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Import"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          description: The uploaded file exceeds the 10 MB public gateway limit.
        "422":
          description: The uploaded file was rejected.
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}:
    get:
      tags: [Imports]
      operationId: getImportDetails
      summary: Get import details
      description: Retrieve the current contact-import state and counters.
      parameters:
        - $ref: "#/components/parameters/ImportId"
      responses:
        "200":
          description: Import details retrieved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportDetails"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}/csv-settings:
    post:
      tags: [Imports]
      operationId: saveImportCsvSettings
      summary: Configure CSV parsing
      description: Configure how the uploaded CSV file is parsed.
      parameters:
        - $ref: "#/components/parameters/ImportId"
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [separator, start_read_at]
              properties:
                separator:
                  type: string
                  minLength: 1
                  maxLength: 1
                  description: CSV separator. Supported values are `;`, `,`, and a tab character.
                  example: ";"
                comment:
                  type: string
                  enum: ["", "#"]
                  description: Optional comment character. Use an empty value when the file has no comments.
                  example: "#"
                start_read_at:
                  type: integer
                  minimum: 0
                  description: Zero-based row index where contact data starts.
                  example: 1
                country:
                  type: string
                  description: Optional ISO 3166-1 alpha-3 country code used for address formatting.
                  example: FRA
      responses:
        "201":
          description: CSV settings saved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Import"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}/columns:
    get:
      tags: [Imports]
      operationId: getImportColumns
      summary: Get CSV column names
      description: Retrieve the CSV row used as column headers.
      parameters:
        - $ref: "#/components/parameters/ImportId"
        - name: separator
          in: query
          required: true
          description: CSV separator. Use the same value as in CSV settings.
          schema:
            type: string
            minLength: 1
            maxLength: 1
            example: ";"
        - name: comment
          in: query
          required: false
          description: Optional comment character.
          schema:
            type: string
            enum: ["", "#"]
            example: "#"
        - name: columns_idx
          in: query
          required: true
          description: Zero-based CSV row index containing the column headers.
          schema:
            type: integer
            minimum: 0
            example: 0
      responses:
        "201":
          description: Column names retrieved
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
              example: ["First Name", "Last Name", "Email", "Mobile Phone"]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}/file-preview:
    get:
      tags: [Imports]
      operationId: previewImportFile
      summary: Preview CSV content
      description: Retrieve up to the first 25 non-empty rows of the CSV file.
      parameters:
        - $ref: "#/components/parameters/ImportId"
        - name: separator
          in: query
          required: true
          description: CSV separator. Use the same value as in CSV settings.
          schema:
            type: string
            minLength: 1
            maxLength: 1
            example: ";"
        - name: comment
          in: query
          required: false
          description: Optional comment character.
          schema:
            type: string
            enum: ["", "#"]
            example: "#"
        - name: start_read_at
          in: query
          required: false
          description: Zero-based row index where the preview starts.
          schema:
            type: integer
            minimum: 0
            default: 0
            example: 0
      responses:
        "201":
          description: File preview retrieved
          content:
            application/json:
              schema:
                type: array
                items:
                  type: array
                  items:
                    type: string
              example:
                - ["First Name", "Last Name", "Email", "Mobile Phone"]
                - ["John", "Doe", "john@example.com", "+33123456789"]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}/properties:
    get:
      tags: [Imports]
      operationId: getImportProperties
      summary: List supported contact fields
      description: |
        List the standard fields accepted as mapping keys. For custom fields, use
        `custom_field:<formId>` or `custom_field:<formId>:<formRefId>`. Retrieve
        `formId` values from `GET /v1/forms/type/custom_fields`; retrieve accepted
        `formRefId` values from `GET /forms/{id}`.
      parameters:
        - $ref: "#/components/parameters/ImportId"
      responses:
        "200":
          description: Supported contact fields retrieved
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
              example:
                - contact_id
                - tag
                - first_name
                - last_name
                - birth_date
                - house_number
                - street
                - city
                - email
                - gender
                - married_name
                - mobile
                - phone
                - age
                - additional_address_fields
                - postal_code
                - country
                - county
                - building
                - building_type
                - floor
                - door
                - birth_country
                - nationality
                - birth_city
                - polling_station
                - note
                - external_id
                - nationbuilderid
                - stripe_id
                - membership_member
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}/mapping:
    post:
      tags: [Imports]
      operationId: saveImportMapping
      summary: Save contact field mapping
      description: |
        Map contact fields to zero-based CSV column indexes. Mapping at least one
        field is required before the import can be started.
      parameters:
        - $ref: "#/components/parameters/ImportId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactMapping"
      responses:
        "201":
          description: Mapping saved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Import"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}/enqueue:
    post:
      tags: [Imports]
      operationId: enqueueImport
      summary: Start import processing
      description: Start asynchronous processing after CSV settings and mapping have been saved.
      parameters:
        - $ref: "#/components/parameters/ImportId"
      responses:
        "201":
          description: Import processing started
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          description: CSV settings, mapping, or import state does not allow processing to start.
        "500":
          $ref: "#/components/responses/InternalServerError"

  /imports/{importId}/statuses:
    get:
      tags: [Imports]
      operationId: listImportStatuses
      summary: Get import status history
      description: |
        Retrieve the complete history of processing statuses. Results are not sorted;
        sort items by `created_at` and use the most recent item as the current status.
      parameters:
        - $ref: "#/components/parameters/ImportId"
      responses:
        "200":
          description: Status history retrieved
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ImportStatus"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
  parameters:
    ImportId:
      name: importId
      in: path
      required: true
      description: Import UUID.
      schema:
        type: string
        format: uuid
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/AuthError"
    BadRequest:
      description: Invalid request parameters or import ID.
      content:
        text/plain:
          schema:
            type: string
          example: Bad Request
    InternalServerError:
      description: Unexpected server-side error.
      content:
        text/plain:
          schema:
            type: string
          example: Internal Server Error
  schemas:
    AuthError:
      type: object
      required: [status, data]
      properties:
        status:
          type: string
          enum: [fail]
        data:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
      example:
        status: fail
        data: missing bearer authorization token

    Import:
      type: object
      required: [id]
      description: Client-facing fields returned when an import is created or configured.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          description: Import name, initialized from the uploaded file name.
        original_file_name:
          type: string
        created_at:
          type: string
          format: date-time
          nullable: true
        start_read_at:
          type: integer
          description: Zero-based row index where contact data starts. `-1` before CSV settings are saved.
        country:
          type: string
        separator_char:
          type: string
        comment_char:
          type: string
        column_indexes:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/ContactMapping"
        csv_settings_updated_at:
          type: string
          format: date-time
          nullable: true
        mapping_defined_at:
          type: string
          format: date-time
          nullable: true
        total_created:
          type: integer
        total_invalid_lines:
          type: integer
        total_already_exists:
          type: integer
        total_errors:
          type: integer
        on_hold_profils:
          type: integer

    ImportDetails:
      type: object
      required: [id, start_read_at, separator_char, comment_char, counters, status]
      description: Current client-facing state returned by GET /imports/{importId}.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        original_file_name:
          type: string
        created_at:
          type: string
          format: date-time
          nullable: true
        start_read_at:
          type: integer
        separator_char:
          type: string
        comment_char:
          type: string
        mapping:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/ContactMapping"
        csv_settings_updated_at:
          type: string
          format: date-time
          nullable: true
        mapping_defined_at:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          description: Current processing status.
          example: finalization_process_done
        counters:
          type: object
          properties:
            total_created:
              type: integer
            total_invalid_lines:
              type: integer
            total_already_exists:
              type: integer
            total_errors:
              type: integer
            total_on_hold_profils:
              type: integer
          additionalProperties:
            type: integer

    ImportStatus:
      type: object
      required: [status, created_at]
      properties:
        status:
          type: string
          example: file_parsing_done
        created_at:
          type: string
          format: date-time
        description:
          type: string
          nullable: true
          example: "total created: 4; total invalid: 0; total already exists: 0; total errors: 0"

    ContactMapping:
      type: object
      minProperties: 1
      description: |
        Map a contact field to a zero-based CSV column index. For custom fields,
        use `custom_field:<formId>` or `custom_field:<formId>:<formRefId>`.
        Retrieve `formId` values from `GET /v1/forms/type/custom_fields` and
        `formRefId` values from `GET /forms/{id}`.
      properties:
        contact_id:
          $ref: "#/components/schemas/ColumnIndex"
        tag:
          $ref: "#/components/schemas/ColumnIndexes"
        first_name:
          $ref: "#/components/schemas/ColumnIndex"
        last_name:
          $ref: "#/components/schemas/ColumnIndex"
        birth_date:
          $ref: "#/components/schemas/ColumnIndex"
        house_number:
          $ref: "#/components/schemas/ColumnIndex"
        street:
          $ref: "#/components/schemas/ColumnIndex"
        city:
          $ref: "#/components/schemas/ColumnIndex"
        email:
          $ref: "#/components/schemas/ColumnIndex"
        gender:
          $ref: "#/components/schemas/ColumnIndex"
        married_name:
          $ref: "#/components/schemas/ColumnIndex"
        mobile:
          $ref: "#/components/schemas/ColumnIndex"
        phone:
          $ref: "#/components/schemas/ColumnIndex"
        age:
          $ref: "#/components/schemas/ColumnIndex"
        additional_address_fields:
          $ref: "#/components/schemas/ColumnIndex"
        postal_code:
          $ref: "#/components/schemas/ColumnIndex"
        country:
          $ref: "#/components/schemas/ColumnIndex"
        county:
          $ref: "#/components/schemas/ColumnIndex"
        building:
          $ref: "#/components/schemas/ColumnIndex"
        building_type:
          $ref: "#/components/schemas/ColumnIndex"
        floor:
          $ref: "#/components/schemas/ColumnIndex"
        door:
          $ref: "#/components/schemas/ColumnIndex"
        birth_country:
          $ref: "#/components/schemas/ColumnIndex"
        nationality:
          $ref: "#/components/schemas/ColumnIndex"
        birth_city:
          $ref: "#/components/schemas/ColumnIndex"
        polling_station:
          $ref: "#/components/schemas/ColumnIndex"
        note:
          $ref: "#/components/schemas/ColumnIndex"
        external_id:
          $ref: "#/components/schemas/ColumnIndex"
        nationbuilderid:
          $ref: "#/components/schemas/ColumnIndex"
        stripe_id:
          $ref: "#/components/schemas/ColumnIndex"
        membership_member:
          $ref: "#/components/schemas/ColumnIndex"
      additionalProperties:
        $ref: "#/components/schemas/ColumnIndexes"
      example:
        first_name: 0
        last_name: 1
        email: 3
        mobile: 4
        "custom_field:123": 5

    ColumnIndex:
      type: integer
      minimum: 0
      description: Zero-based CSV column index.

    ColumnIndexes:
      oneOf:
        - $ref: "#/components/schemas/ColumnIndex"
        - type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/ColumnIndex"
