openapi: 3.1.0
info:
  title: FYNKHA Public API
  version: 1.0.0
  description: API B2B multi-sociétés pour connecter un ERP, un site ou un partenaire à FYNKHA Transport OS.
  contact:
    name: Support intégrations FYNKHA
    email: support@fynkha.com
  license:
    name: Propriétaire — utilisation soumise au contrat FYNKHA
servers:
  - url: https://api.fynkha.com/api/public/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Company
    description: Identité et capacités de la société liée à la clé.
  - name: Clients
    description: Référentiel clients de la société.
  - name: Quotes
    description: Devis commerciaux de la société.
  - name: Quote requests
    description: Demandes entrantes à traiter dans FYNKHA.
  - name: Fleet
    description: Matériels, tournées et données carburant autorisées.
  - name: Missions
    description: Missions opérationnelles et affectations.
paths:
  /company:
    get:
      operationId: getCompany
      tags: [Company]
      summary: Lire l’identité et les modules autorisés de la société
      x-fynkha-scope: company:read
      responses:
        '200':
          description: Société liée à la clé API
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: '#/components/schemas/Company'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /clients:
    get:
      operationId: listClients
      tags: [Clients]
      summary: Lister les clients de la société
      x-fynkha-scope: clients:read
      parameters:
        - { name: search, in: query, schema: { type: string, maxLength: 255 } }
        - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200':
          description: Liste paginée
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClientCollection'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /quotes:
    get:
      operationId: listQuotes
      tags: [Quotes]
      summary: Lister les devis de la société
      x-fynkha-scope: quotes:read
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200':
          description: Liste paginée
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Quote' } }
                  meta: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /quote-requests:
    post:
      operationId: createQuoteRequest
      tags: [Quote requests]
      summary: Créer une demande de devis
      x-fynkha-scope: quote_requests:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateQuoteRequest'
      responses:
        '201':
          description: Demande créée, ou réponse originale rejouée
          headers:
            Idempotency-Replayed:
              schema: { type: string, enum: ['true'] }
              description: Présent uniquement lors d’un rejeu
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/QuoteRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: Une requête de même clé est encore en cours
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
        '422':
          description: Validation ou réutilisation conflictuelle de la clé
          content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
  /fleet/vehicles:
    get:
      operationId: listFleetVehicles
      tags: [Fleet]
      summary: Lister les véhicules et remorques de la société
      x-fynkha-scope: fleet:read
      parameters:
        - { name: category, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string } }
        - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200': { description: Liste paginée des matériels }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /missions:
    get:
      operationId: listMissions
      tags: [Missions]
      summary: Lister les missions et leurs affectations
      x-fynkha-scope: missions:read
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200': { description: Liste paginée des missions }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /fleet/tours:
    get:
      operationId: listFleetTours
      tags: [Fleet]
      summary: Lister les tournées, leurs missions et leurs arrêts
      x-fynkha-scope: fleet:read
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200':
          description: Tournées paginées, sans notes internes ni données de coût
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/FleetTour' } }
                  meta: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /fleet/fuel-transactions:
    get:
      operationId: listFuelTransactions
      tags: [Fleet]
      summary: Lister les transactions carburant et leur rapprochement
      x-fynkha-scope: fleet:read
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [unmatched, matched, anomaly, ignored] } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
      responses:
        '200':
          description: Transactions paginées sans numéro de carte, identifiant fournisseur privé ni contenu brut
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/FuelTransaction' } }
                  meta: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: FYNKHA_API_TOKEN
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 1, maxLength: 120 }
      description: Clé unique conservée 24 heures. Une même clé doit toujours accompagner exactement la même requête.
  responses:
    Unauthorized:
      description: Jeton absent, invalide, expiré ou révoqué
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    Forbidden:
      description: Scope ou abonnement insuffisant
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    RateLimited:
      description: Limite par minute ou quota mensuel dépassé
      headers:
        Retry-After: { schema: { type: integer } }
        X-FYNKHA-Quota-Limit: { schema: { type: string } }
        X-FYNKHA-Quota-Remaining: { schema: { type: string } }
        X-FYNKHA-Quota-Reset: { schema: { type: string, format: date-time } }
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
  schemas:
    Company:
      type: object
      required: [id, name, business_profile, enabled_modules]
      properties:
        id: { type: integer }
        name: { type: string }
        code: { type: [string, 'null'] }
        business_profile: { type: string, enum: [gestion, carrier, forwarder, hybrid, enterprise] }
        currency: { type: string, minLength: 3, maxLength: 3 }
        timezone: { type: string }
        enabled_modules: { type: array, items: { type: string } }
    Client:
      type: object
      properties:
        id: { type: integer }
        code: { type: [string, 'null'] }
        name: { type: string }
        email: { type: [string, 'null'], format: email }
        phone: { type: [string, 'null'] }
        city: { type: [string, 'null'] }
        country_code: { type: [string, 'null'] }
        is_active: { type: boolean }
        updated_at: { type: [string, 'null'], format: date-time }
    ClientCollection:
      type: object
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Client' } }
        meta: { $ref: '#/components/schemas/Pagination' }
    Quote:
      type: object
      properties:
        id: { type: string, format: uuid }
        number: { type: [string, 'null'] }
        status: { type: string }
        client_id: { type: [integer, 'null'] }
        issue_date: { type: [string, 'null'], format: date }
        valid_until: { type: [string, 'null'], format: date }
        transport_mode: { type: [string, 'null'] }
        pickup_city: { type: [string, 'null'] }
        delivery_city: { type: [string, 'null'] }
        subtotal_ht: { type: [string, 'null'] }
        total_ttc: { type: [string, 'null'] }
        currency: { type: string }
        updated_at: { type: [string, 'null'], format: date-time }
    CreateQuoteRequest:
      type: object
      required: [external_reference, transport_mode, pickup_city, delivery_city, goods_description]
      properties:
        external_reference: { type: string, maxLength: 120 }
        client_id: { type: integer }
        prospect_name: { type: string }
        prospect_company: { type: string }
        prospect_email: { type: string, format: email }
        prospect_phone: { type: string }
        transport_mode: { type: string, enum: [road, maritime, air, rail, multimodal] }
        pickup_address: { type: string }
        pickup_city: { type: string }
        pickup_postal_code: { type: string }
        pickup_country_code: { type: string, minLength: 2, maxLength: 2 }
        pickup_date: { type: string, format: date }
        delivery_address: { type: string }
        delivery_city: { type: string }
        delivery_postal_code: { type: string }
        delivery_country_code: { type: string, minLength: 2, maxLength: 2 }
        goods_description: { type: string }
        packages: { type: integer, minimum: 0 }
        pallets: { type: integer, minimum: 0 }
        weight: { type: number, minimum: 0 }
        volume: { type: number, minimum: 0 }
        notes: { type: string }
    QuoteRequest:
      type: object
      properties:
        id: { type: integer }
        external_reference: { type: string }
        status: { type: string }
        created_at: { type: string, format: date-time }
    FleetTour:
      type: object
      required: [id, number, status, missions, stops]
      properties:
        id: { type: integer }
        number: { type: string }
        name: { type: [string, 'null'] }
        status: { type: string }
        scheduled_start_at: { type: [string, 'null'], format: date-time }
        scheduled_end_at: { type: [string, 'null'], format: date-time }
        start_location: { type: [string, 'null'] }
        end_location: { type: [string, 'null'] }
        estimated_distance_km: { type: [string, 'null'] }
        route_plan: { oneOf: [{ $ref: '#/components/schemas/FleetRoutePlan' }, { type: 'null' }] }
        driver: { type: [object, 'null'] }
        vehicle: { type: [object, 'null'] }
        trailer: { type: [object, 'null'] }
        missions: { type: array, items: { type: object } }
        stops: { type: array, items: { $ref: '#/components/schemas/FleetTourStop' } }
        updated_at: { type: [string, 'null'], format: date-time }
    FleetRoutePlan:
      type: object
      properties:
        provider: { type: string, enum: [here, heuristic] }
        status: { type: string, enum: [compliant, warning, blocked] }
        estimated_driving_minutes: { type: integer, minimum: 0 }
        traffic_delay_minutes: { type: integer, minimum: 0 }
        required_break_minutes: { type: integer, minimum: 0 }
        weekly_driving_minutes_after: { type: integer, minimum: 0 }
        fortnight_driving_minutes_after: { type: integer, minimum: 0 }
        extended_daily_driving_days_after: { type: integer, minimum: 0 }
        traffic_incident_count: { type: integer, minimum: 0 }
        planned_work_end_at: { type: [string, 'null'], format: date-time }
        calculated_at: { type: string, format: date-time }
        warnings: { type: array, items: { type: string } }
    FleetTourStop:
      type: object
      properties:
        id: { type: integer }
        mission_id: { type: [integer, 'null'] }
        sequence: { type: integer }
        type: { type: string, enum: [pickup, delivery] }
        name: { type: [string, 'null'] }
        address: { type: string }
        window_start_at: { type: [string, 'null'], format: date-time }
        window_end_at: { type: [string, 'null'], format: date-time }
        estimated_arrival_at: { type: [string, 'null'], format: date-time }
        planned_departure_at: { type: [string, 'null'], format: date-time }
        leg_distance_km: { type: [string, 'null'] }
        leg_driving_minutes: { type: [integer, 'null'], minimum: 0 }
        leg_traffic_delay_minutes: { type: [integer, 'null'], minimum: 0 }
        cumulative_break_minutes: { type: integer, minimum: 0 }
        actual_arrival_at: { type: [string, 'null'], format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        status: { type: string }
    FuelTransaction:
      type: object
      required: [id, external_reference, provider, occurred_at, status, anomaly_codes]
      properties:
        id: { type: integer }
        external_reference: { type: string }
        provider: { type: string }
        occurred_at: { type: string, format: date-time }
        merchant: { type: [string, 'null'] }
        country_code: { type: [string, 'null'] }
        product_type: { type: string }
        quantity_liters: { type: [string, 'null'] }
        amount_ht: { type: string }
        vat_amount: { type: string }
        currency: { type: string }
        status: { type: string, enum: [unmatched, matched, anomaly, ignored] }
        anomaly_codes: { type: array, items: { type: string } }
        vehicle: { type: [object, 'null'] }
        mission: { type: [object, 'null'] }
        updated_at: { type: [string, 'null'], format: date-time }
    Pagination:
      type: object
      properties:
        current_page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        last_page: { type: integer }
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            required_scope: { type: string }
