openapi: 3.1.0
info:
  title: API publique Noltya
  version: 1.0.0
  description: >-
    API tenantée pour les automatisations installateurs, régies, Make, Zapier
    et SI partenaires. Le workspace et les scopes proviennent exclusivement de
    la clé API.
servers:
  - url: https://api.noltya.fr/api/public-api/v1
    description: Production
security:
  - bearerApiKey: []
tags:
  - name: Connexion
  - name: Leads
  - name: Dossiers
  - name: Clients
  - name: Rendez-vous
  - name: Devis
  - name: Factures
  - name: Paiements
  - name: Documents
  - name: Partenaires CEE
paths:
  /connection:
    get:
      operationId: testConnection
      summary: Tester une clé API
      description: >-
        Valide la clé et retourne son libellé et ses scopes sans exposer
        d'identifiant de workspace ni de secret.
      tags: [Connexion]
      responses:
        "200":
          description: Clé valide.
          headers: &AuthenticatedRateLimitHeaders
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining:
              { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
            X-Noltya-RateLimit-Scope:
              { $ref: "#/components/headers/RateLimitScope" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Connection"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /leads:
    get:
      operationId: listLeads
      summary: Lister les leads
      tags: [Leads]
      x-noltya-required-scope: leads:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page de leads, triée du plus récent au plus ancien.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining:
              { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
            X-Noltya-RateLimit-Scope:
              { $ref: "#/components/headers/RateLimitScope" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
    post:
      operationId: createLead
      summary: Créer un lead de façon idempotente
      tags: [Leads]
      x-noltya-required-scope: leads:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateLead"
      responses:
        "201":
          description: Lead créé ou réponse rejouée.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining:
              { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
            X-Noltya-RateLimit-Scope:
              { $ref: "#/components/headers/RateLimitScope" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Lead"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /dossiers:
    get:
      operationId: listDossiers
      summary: Lister les dossiers
      tags: [Dossiers]
      x-noltya-required-scope: dossiers:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page de dossiers, triée du plus récent au plus ancien.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining:
              { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
            X-Noltya-RateLimit-Scope:
              { $ref: "#/components/headers/RateLimitScope" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DossierPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /leads/{id}:
    get:
      operationId: getLead
      summary: Lire un élément leads
      tags: [Leads]
      x-noltya-required-scope: leads:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Leads du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Lead"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /dossiers/{id}:
    get:
      operationId: getDossier
      summary: Lire un élément dossiers
      tags: [Dossiers]
      x-noltya-required-scope: dossiers:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Dossiers du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Dossier"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /customers:
    get:
      operationId: listCustomers
      summary: Lister les clients
      tags: [Clients]
      x-noltya-required-scope: customers:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page tenantée de clients.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /customers/{id}:
    get:
      operationId: getCustomer
      summary: Lire un élément clients
      tags: [Clients]
      x-noltya-required-scope: customers:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Clients du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Customer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /appointments:
    get:
      operationId: listAppointments
      summary: Lister les rendez-vous
      tags: [Rendez-vous]
      x-noltya-required-scope: appointments:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page tenantée de rendez-vous.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AppointmentPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /appointments/{id}:
    get:
      operationId: getAppointment
      summary: Lire un élément rendez-vous
      tags: [Rendez-vous]
      x-noltya-required-scope: appointments:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Rendez-vous du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Appointment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /quotes:
    get:
      operationId: listQuotes
      summary: Lister les devis
      tags: [Devis]
      x-noltya-required-scope: quotes:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page tenantée de devis.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QuotePage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /quotes/{id}:
    get:
      operationId: getQuote
      summary: Lire un élément devis
      tags: [Devis]
      x-noltya-required-scope: quotes:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Devis du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /invoices:
    get:
      operationId: listInvoices
      summary: Lister les factures
      tags: [Factures]
      x-noltya-required-scope: invoices:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page tenantée de factures.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoicePage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /invoices/{id}:
    get:
      operationId: getInvoice
      summary: Lire un élément factures
      tags: [Factures]
      x-noltya-required-scope: invoices:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Factures du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /credit-notes:
    get:
      operationId: listCreditNotes
      summary: Lister les factures
      tags: [Factures]
      x-noltya-required-scope: invoices:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page tenantée de factures.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvoicePage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /credit-notes/{id}:
    get:
      operationId: getCreditNote
      summary: Lire un élément factures
      tags: [Factures]
      x-noltya-required-scope: invoices:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Factures du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invoice"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /payments:
    get:
      operationId: listPayments
      summary: Lister les paiements
      tags: [Paiements]
      x-noltya-required-scope: payments:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Page tenantée de paiements.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /payments/{id}:
    get:
      operationId: getPayment
      summary: Lire un élément paiements
      tags: [Paiements]
      x-noltya-required-scope: payments:read
      x-noltya-optional-scopes: [contacts:read]
      parameters:
        - $ref: "#/components/parameters/ResourceId"
      responses:
        "200":
          description: Paiements du workspace de la clé.
          headers: *AuthenticatedRateLimitHeaders
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"

  /dossiers/{dossierId}/documents:
    post:
      operationId: createDossierDocument
      summary: Déposer un document contrôlé dans un dossier
      tags: [Documents]
      x-noltya-required-scope: documents:write
      parameters:
        - name: dossierId
          in: path
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 120
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: false
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF ou image, 10 Mo maximum.
                title:
                  type: string
                  maxLength: 180
                category:
                  type: string
                  enum:
                    [
                      Identite,
                      Fiscal,
                      Adresse,
                      Banque,
                      Technique,
                      Contrat,
                      Facturation,
                      Autre,
                    ]
                documentType:
                  type: string
                  enum:
                    [
                      Piece d identite,
                      Justificatif de domicile,
                      RIB,
                      Avis d impot,
                      Facture EDF,
                      Photo compteur Linky,
                      Photo tableau electrique,
                      PV de reception,
                      Contrat signe,
                      Autre,
                    ]
                notes:
                  type: string
                  maxLength: 2000
      responses:
        "201":
          description: Document reçu après les contrôles du pipeline Noltya.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining:
              { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
            X-Noltya-RateLimit-Scope:
              { $ref: "#/components/headers/RateLimitScope" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          description: Fichier supérieur à la limite publique.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
  /cee-partners/{organizationId}/tariffs:
    put:
      operationId: replaceCeePartnerTariffs
      summary: Remplacer atomiquement une grille CEE privée
      tags: [Partenaires CEE]
      x-noltya-required-scope: cee:tariffs:write
      parameters:
        - name: organizationId
          in: path
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 100
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReplaceCeeTariffs"
      responses:
        "200":
          description: Grille remplacée ou réponse rejouée.
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            RateLimit-Remaining:
              { $ref: "#/components/headers/RateLimitRemaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
            RateLimit-Policy: { $ref: "#/components/headers/RateLimitPolicy" }
            X-Noltya-RateLimit-Scope:
              { $ref: "#/components/headers/RateLimitScope" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CeeTariffFeedResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/QuotaUnavailable"
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: nlt_live
      description: Clé affichée une seule fois lors de sa création.
  parameters:
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
        maxLength: 300
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    ResourceId:
      name: id
      in: path
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 120
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: 8 à 200 caractères ASCII; unique par clé API et opération.
      schema:
        type: string
        minLength: 8
        maxLength: 200
        pattern: '^[\x21-\x7e]+$'
  headers:
    RateLimitLimit:
      description: Limite de la politique actuellement contraignante.
      schema:
        type: integer
    RateLimitRemaining:
      description: Tentatives restantes dans la fenêtre UTC courante.
      schema:
        type: integer
        minimum: 0
    RateLimitReset:
      description: Secondes avant la prochaine fenêtre UTC.
      schema:
        type: integer
        minimum: 1
    RateLimitPolicy:
      description: Politiques par clé et par workspace appliquées à la requête.
      schema:
        type: string
    RateLimitScope:
      description: Politique actuellement contraignante.
      schema:
        type: string
        enum: [key, workspace]
    RetryAfter:
      description: Secondes avant de réessayer.
      schema:
        type: integer
        minimum: 1
  responses:
    BadRequest:
      description: Requête invalide.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Clé absente, invalide, révoquée ou expirée.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Scope requis absent.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Ressource introuvable dans le workspace de la clé.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Conflit d'idempotence, doublon ou version obsolète.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    QuotaUnavailable:
      description: Le stockage de quota ou son verrou est momentanément indisponible; la requête métier n'est pas exécutée.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Quota par minute de la clé ou du workspace dépassé. La tentative refusée est comptée.
      headers:
        RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        RateLimit-Policy:
          $ref: "#/components/headers/RateLimitPolicy"
        X-Noltya-RateLimit-Scope:
          $ref: "#/components/headers/RateLimitScope"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Connection:
      type: object
      additionalProperties: false
      required: [connected, connectionName, scopes]
      properties:
        connected:
          type: boolean
          const: true
        connectionName:
          type: string
        scopes:
          type: array
          uniqueItems: true
          items:
            type: string
            enum:
              [
                "leads:read",
                "leads:write",
                "dossiers:read",
                "customers:read",
                "appointments:read",
                "quotes:read",
                "invoices:read",
                "payments:read",
                "contacts:read",
                "cee:tariffs:write",
                "documents:write",
              ]
    Error:
      type: object
      properties:
        message:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
        code:
          type: string
    CreateLead:
      type: object
      additionalProperties: false
      anyOf:
        - required: [company]
        - required: [contact]
        - required: [firstName]
        - required: [lastName]
      properties:
        company:
          type: string
          maxLength: 120
        firstName:
          type: string
          maxLength: 100
        lastName:
          type: string
          maxLength: 100
        contact:
          type: string
          maxLength: 200
        email:
          type: string
          format: email
          maxLength: 254
        phone:
          type: string
          maxLength: 40
        addressLine1:
          type: string
          maxLength: 200
        postcode:
          type: string
          maxLength: 20
        city:
          type: string
          maxLength: 120
        source:
          type: string
          maxLength: 120
        campaign:
          type: string
          maxLength: 120
        product:
          type: string
          maxLength: 120
        notes:
          type: string
          maxLength: 5000
    Lead:
      type: object
      required: [id, status, source, ownerId, createdAt, updatedAt]
      properties:
        id: { type: string }
        firstName: { type: string }
        lastName: { type: string }
        company: { type: string }
        status: { type: string }
        source: { type: string }
        ownerId:
          type: [string, "null"]
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        email: { type: string, format: email }
        phone: { type: string }
        address: { type: string }
        city: { type: string }
        postalCode: { type: string }
    LeadPage:
      type: object
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Lead"
        nextCursor:
          type: [string, "null"]
    Dossier:
      type: object
      required: [id, reference, status, ownerId, createdAt, updatedAt]
      properties:
        id: { type: string }
        reference: { type: string }
        title: { type: string }
        status: { type: string }
        customerName: { type: string }
        ownerId:
          type: [string, "null"]
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        customerEmail: { type: string, format: email }
        customerPhone: { type: string }
        address: { type: string }
        city: { type: string }
        postalCode: { type: string }
    DossierPage:
      type: object
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Dossier"
        nextCursor:
          type: [string, "null"]
    Customer:
      type: object
      additionalProperties: false
      required:
        [
          id,
          quoteId,
          product,
          ownerId,
          stage,
          documentsProgress,
          documentsStatus,
          paymentStatus,
          createdAt,
          updatedAt,
        ]
      properties:
        id: { type: string }
        quoteId: { type: [string, "null"] }
        product: { type: string }
        ownerId: { type: [string, "null"] }
        stage: { type: string }
        documentsProgress: { type: integer }
        documentsStatus: { type: string }
        paymentStatus: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        contact: { type: string }
        city: { type: string }
    CustomerPage:
      type: object
      additionalProperties: false
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Customer"
        nextCursor: { type: [string, "null"] }
    Appointment:
      type: object
      additionalProperties: false
      required:
        [
          id,
          customerId,
          leadId,
          dossierId,
          day,
          start,
          durationMinutes,
          type,
          status,
          ownerId,
          confirmationStatus,
          createdAt,
          updatedAt,
        ]
      properties:
        id: { type: string }
        customerId: { type: [string, "null"] }
        leadId: { type: [string, "null"] }
        dossierId: { type: [string, "null"] }
        day: { type: string }
        start: { type: string }
        durationMinutes: { type: integer, minimum: 1 }
        type: { type: string }
        status: { type: [string, "null"] }
        ownerId: { type: [string, "null"] }
        confirmationStatus: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        customer: { type: string }
        phone: { type: string }
        address: { type: string }
        city: { type: string }
        postalCode: { type: string }
        notes: { type: string }
    AppointmentPage:
      type: object
      additionalProperties: false
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Appointment"
        nextCursor: { type: [string, "null"] }
    QuoteLine:
      type: object
      additionalProperties: false
      required:
        [
          id,
          position,
          title,
          description,
          quantity,
          unit,
          unitPriceExclTaxCents,
          vatRateBps,
          discountCents,
        ]
      properties:
        id: { type: string }
        position: { type: integer }
        title: { type: string }
        description: { type: string }
        quantity: { type: number }
        unit: { type: string }
        unitPriceExclTaxCents: { type: integer }
        vatRateBps: { type: integer }
        discountCents: { type: integer }
    Quote:
      type: object
      additionalProperties: false
      required:
        [
          id,
          quoteNumber,
          opportunityId,
          product,
          ownerId,
          status,
          pdfStatus,
          grossAmountCents,
          netAmountCents,
          validUntil,
          signedAt,
          createdAt,
          updatedAt,
        ]
      properties:
        id: { type: string }
        quoteNumber: { type: [string, "null"] }
        opportunityId: { type: [string, "null"] }
        product: { type: string }
        ownerId: { type: [string, "null"] }
        status: { type: string }
        pdfStatus: { type: string }
        grossAmountCents: { type: integer }
        netAmountCents: { type: integer }
        validUntil: { type: [string, "null"], format: date-time }
        signedAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        lines:
          type: array
          items:
            $ref: "#/components/schemas/QuoteLine"
        customer: { type: string }
        contactName: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        address: { type: string }
        city: { type: string }
        postalCode: { type: string }
    QuotePage:
      type: object
      additionalProperties: false
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Quote"
        nextCursor: { type: [string, "null"] }
    InvoiceLine:
      type: object
      additionalProperties: false
      required:
        [
          id,
          position,
          title,
          description,
          quantity,
          unit,
          unitPriceExclTaxCents,
          discountCents,
          vatRateBps,
          totalExclTaxCents,
          vatAmountCents,
          totalInclTaxCents,
        ]
      properties:
        id: { type: string }
        position: { type: integer }
        title: { type: string }
        description: { type: string }
        quantity: { type: number }
        unit: { type: string }
        unitPriceExclTaxCents: { type: integer }
        discountCents: { type: integer }
        vatRateBps: { type: integer }
        totalExclTaxCents: { type: integer }
        vatAmountCents: { type: integer }
        totalInclTaxCents: { type: integer }
    BillingAddress:
      type: object
      additionalProperties: false
      required: [line1, line2, postalCode, city, countryCode]
      properties:
        line1: { type: string }
        line2: { type: string }
        postalCode: { type: string }
        city: { type: string }
        countryCode: { type: string }
    Invoice:
      type: object
      additionalProperties: false
      required:
        [
          id,
          quoteId,
          documentType,
          invoiceKind,
          status,
          paymentStatus,
          currency,
          invoiceNumber,
          issueDate,
          issuedAt,
          dueDate,
          totalExclTaxCents,
          vatAmountCents,
          totalInclTaxCents,
          paidAmountCents,
          sourceInvoiceId,
          createdAt,
          updatedAt,
        ]
      properties:
        id: { type: string }
        quoteId: { type: string }
        documentType: { type: string, enum: [INVOICE, CREDIT_NOTE] }
        invoiceKind: { type: string, enum: [DEPOSIT, FINAL, STANDARD] }
        status: { type: string, enum: [DRAFT, ISSUED] }
        paymentStatus:
          type: string
          enum: [UNPAID, PARTIAL, PAID, NOT_APPLICABLE]
        currency: { type: string }
        invoiceNumber: { type: [string, "null"] }
        issueDate: { type: [string, "null"], format: date-time }
        issuedAt: { type: [string, "null"], format: date-time }
        dueDate: { type: [string, "null"], format: date-time }
        totalExclTaxCents: { type: integer }
        vatAmountCents: { type: integer }
        totalInclTaxCents: { type: integer }
        paidAmountCents: { type: integer }
        sourceInvoiceId: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        lines:
          type: array
          items:
            $ref: "#/components/schemas/InvoiceLine"
        customerLabel: { type: string }
        buyerSiren: { type: string }
        buyerVatNumber: { type: string }
        billingAddress:
          $ref: "#/components/schemas/BillingAddress"
    InvoicePage:
      type: object
      additionalProperties: false
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Invoice"
        nextCursor: { type: [string, "null"] }
    Payment:
      type: object
      additionalProperties: false
      required:
        [
          id,
          invoiceId,
          amountCents,
          reversedAmountCents,
          netAmountCents,
          receivedAt,
          method,
          createdAt,
        ]
      properties:
        id: { type: string }
        invoiceId: { type: string }
        amountCents: { type: integer }
        reversedAmountCents: { type: integer, minimum: 0 }
        netAmountCents: { type: integer }
        receivedAt: { type: string, format: date-time }
        method: { type: string }
        createdAt: { type: string, format: date-time }
        reference: { type: string }
    PaymentPage:
      type: object
      additionalProperties: false
      required: [data, nextCursor]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Payment"
        nextCursor: { type: [string, "null"] }

    Document:
      type: object
      required:
        [
          id,
          dossierId,
          title,
          fileName,
          mimeType,
          sizeBytes,
          status,
          uploadedAt,
        ]
      properties:
        id: { type: string }
        dossierId: { type: string }
        title: { type: string }
        fileName: { type: string }
        mimeType: { type: string }
        sizeBytes: { type: integer, minimum: 0 }
        status: { type: string, const: Recu }
        uploadedAt: { type: string, format: date-time }
    ReplaceCeeTariffs:
      type: object
      additionalProperties: false
      required: [version, publishedAt, tariffs]
      properties:
        version:
          type: string
          minLength: 1
          maxLength: 100
        publishedAt:
          type: string
          format: date-time
        tariffs:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            additionalProperties: true
    CeeTariffFeedResult:
      type: object
      required:
        [
          organizationId,
          organizationName,
          version,
          publishedAt,
          syncedAt,
          tariffCount,
        ]
      properties:
        organizationId: { type: string }
        organizationName: { type: string }
        version: { type: string }
        publishedAt: { type: string, format: date-time }
        syncedAt: { type: string, format: date-time }
        tariffCount: { type: integer, minimum: 1, maximum: 100 }
