openapi: 3.1.0
info:
  title: Spedisci.online Client API
  version: "2026-09"
  description: |
    API per i clienti della piattaforma: spedizioni con etichetta, preventivi, tracking, chiusura giornata,
    ritiri, giacenze, punti di consegna, codici doganali e webhook.

    Autenticazione OAuth2 (scope `client-api`) con `client_credentials` (App del cliente).
    Tutte le risorse sono filtrate sul cliente autenticato.
    Documentazione: https://apidocs.spedisci.online/api-2026-09
servers:
  - url: https://{subdomain}.spedisci.online/api/2026-09
    variables:
      subdomain:
        default: demo

security:
  - oauth2: [client-api]

tags:
  - name: Carriers
  - name: Rates
  - name: Shipments
  - name: Tracking
  - name: Pickups
  - name: Manifests
  - name: Stocks
    description: Giacenze
  - name: Delivery points
  - name: Customs
  - name: Webhooks

paths:
  /carriers:
    get:
      tags: [Carriers]
      summary: Corrieri e contratti disponibili per il cliente
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/CarrierWithContracts" }

  /rates:
    post:
      tags: [Rates]
      summary: Preventivo per ogni contratto disponibile
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RatesRequest" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Rate" }
        "422": { $ref: "#/components/responses/ValidationFailed" }

  /shipments:
    get:
      tags: [Shipments]
      summary: Elenco spedizioni
      parameters:
        - { name: from, in: query, schema: { type: string, format: date }, description: "default: 30 giorni fa" }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: status, in: query, schema: { type: string }, description: "chiave o codice, anche lista separata da virgola (es. delivered,held_at_depot)" }
        - { name: carrierCode, in: query, schema: { type: string } }
        - { name: serviceType, in: query, schema: { type: string } }
        - { name: trackingNumber, in: query, schema: { type: string } }
        - { name: reference, in: query, schema: { type: string } }
        - { name: recipientName, in: query, schema: { type: string } }
        - { name: city, in: query, schema: { type: string } }
        - { name: postalCode, in: query, schema: { type: string } }
        - { name: countryCode, in: query, schema: { type: string, minLength: 2, maxLength: 2 } }
        - { name: hasCod, in: query, schema: { type: boolean } }
        - { name: hasInsurance, in: query, schema: { type: boolean } }
        - { name: storeId, in: query, schema: { type: integer } }
        - { $ref: "#/components/parameters/perPage" }
        - { $ref: "#/components/parameters/page" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Paginated"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Shipment" }
    post:
      tags: [Shipments]
      summary: Crea una spedizione (LDV) e restituisce l'etichetta
      description: |
        `customsDetail` è obbligatorio quando la destinazione è fuori dal territorio doganale UE
        (paesi extra UE e territori speciali come Canarie, Ceuta, Melilla, Livigno, Helgoland, Åland).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ShipmentRequest" }
      responses:
        "201":
          description: Spedizione creata
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Shipment"
                  - type: object
                    properties:
                      label: { $ref: "#/components/schemas/Label" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }

  /shipments/{ldv}:
    parameters:
      - { $ref: "#/components/parameters/ldv" }
    get:
      tags: [Shipments]
      summary: Dettaglio spedizione
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Shipment" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Shipments]
      summary: Annulla la spedizione e rimborsa il credito
      responses:
        "200":
          description: Annullata
          content:
            application/json:
              schema:
                type: object
                properties:
                  trackingNumber: { type: string }
                  cancelled: { type: boolean }
                  refunded: { type: number }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Spedizione già in distinta o in transito
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /shipments/{ldv}/label:
    get:
      tags: [Shipments]
      summary: Etichetta in base64
      parameters:
        - { $ref: "#/components/parameters/ldv" }
        - { name: format, in: query, schema: { type: string, enum: [pdf, zpl] }, description: "omesso: entrambi i formati disponibili" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      trackingNumber: { type: string }
                  - $ref: "#/components/schemas/Label"
        "404": { $ref: "#/components/responses/NotFound" }

  /shipments/{ldv}/tracking:
    get:
      tags: [Tracking]
      summary: Tracking della spedizione
      parameters:
        - { $ref: "#/components/parameters/ldv" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tracking" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/CarrierError" }

  /tracking/bda/{bda}:
    get:
      tags: [Tracking]
      summary: Tracking per BDA (ultimi 60 giorni)
      parameters:
        - { name: bda, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tracking" }
        "404": { $ref: "#/components/responses/NotFound" }

  /manifests:
    post:
      tags: [Manifests]
      summary: Chiusura giornata (bordero)
      description: Un manifest per contratto; sono incluse solo le spedizioni del cliente non ancora in distinta.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                shipmentIds: { type: array, items: { type: integer } }
                trackingNumbers: { type: array, items: { type: string } }
      responses:
        "201":
          description: Manifest creati
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Manifest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "502": { $ref: "#/components/responses/CarrierError" }

  /manifests/{id}:
    get:
      tags: [Manifests]
      summary: Dettaglio manifest
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: pdf, in: query, schema: { type: boolean }, description: includi il PDF in base64 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Manifest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /stocks:
    get:
      tags: [Stocks]
      summary: Giacenze
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [open, instructed, closed, all], default: open } }
        - { name: trackingNumber, in: query, schema: { type: string } }
        - { $ref: "#/components/parameters/perPage" }
        - { $ref: "#/components/parameters/page" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Paginated"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Stock" }

  /stocks/{id}:
    get:
      tags: [Stocks]
      summary: Dettaglio giacenza
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Stock" }
        "404": { $ref: "#/components/responses/NotFound" }

  /stocks/{id}/actions:
    post:
      tags: [Stocks]
      summary: Istruzioni di svincolo
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StockActionRequest" }
      responses:
        "201":
          description: Istruzioni registrate (e inviate al corriere quando supportato)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Stock" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Istruzioni già inviate
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "502": { $ref: "#/components/responses/CarrierError" }

  /delivery-points:
    get:
      tags: [Delivery points]
      summary: Punti di ritiro/consegna vicino a un indirizzo (BRT)
      parameters:
        - { name: city, in: query, required: true, schema: { type: string } }
        - { name: postalCode, in: query, required: true, schema: { type: string } }
        - { name: address, in: query, schema: { type: string } }
        - { name: carrier, in: query, schema: { type: string, enum: [brt] } }
      responses:
        "200":
          description: Risposta del corriere
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {}
        "502": { $ref: "#/components/responses/CarrierError" }

  /taric:
    get:
      tags: [Customs]
      summary: Ricerca codici HS/TARIC
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 3 } }
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        code: { type: string }
                        description: { type: string }
                        descriptionEn: { type: [string, "null"] }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: Endpoint registrati
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Webhook" }
    post:
      tags: [Webhooks]
      summary: Registra un endpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                description: { type: string }
                events:
                  type: array
                  items: { type: string, enum: [tracking.update, shipment.created, stock.opened, stock.closed] }
      responses:
        "201":
          description: Creato (il `secret` è restituito solo qui)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "422": { $ref: "#/components/responses/ValidationFailed" }

  /webhooks/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [Webhooks]
      summary: Dettaglio
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Webhooks]
      summary: Elimina l'endpoint e i suoi log
      responses:
        "204": { description: Eliminato }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks/{id}/deliveries:
    get:
      tags: [Webhooks]
      summary: Invii degli ultimi 10 giorni
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { $ref: "#/components/parameters/perPage" }
        - { $ref: "#/components/parameters/page" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Paginated"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/WebhookDelivery" }

  /webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Invia un evento tracking.update reale, firmato
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: Esito dell'invio
          content:
            application/json:
              schema:
                type: object
                properties:
                  delivered: { type: boolean }
                  responseCode: { type: integer }
                  responseBody: { type: string }
                  payload: { type: object }
        "404": { $ref: "#/components/responses/NotFound" }

  /pickups:
    get:
      tags: [Pickups]
      summary: Ritiri richiesti (default ultimi 30 giorni)
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { $ref: "#/components/parameters/perPage" }
        - { $ref: "#/components/parameters/page" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Paginated"
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: "#/components/schemas/Pickup" } }
    post:
      tags: [Pickups]
      summary: Richiesta di ritiro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PickupRequest" }
      responses:
        "201":
          description: Ritiro registrato
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pickup" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "422": { $ref: "#/components/responses/ValidationFailed" }

  /pickups/{id}:
    get:
      tags: [Pickups]
      summary: Dettaglio ritiro
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pickup" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    oauth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://{subdomain}.spedisci.online/oauth/token
          scopes:
            client-api: Accesso alle API cliente
  parameters:
    ldv:
      name: ldv
      in: path
      required: true
      description: Numero LDV / tracking number
      schema: { type: string }
    perPage:
      name: per_page
      in: query
      schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
    page:
      name: page
      in: query
      schema: { type: integer, default: 1 }

  responses:
    Unauthenticated:
      description: Token mancante, scaduto o revocato
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Scope, ruolo o stato account non validi
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Risorsa inesistente o di un altro cliente
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ValidationFailed:
      description: Payload non valido (`validation_failed`) o spedizione rifiutata (`shipment_rejected`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PaymentRequired:
      description: Abbonamento richiesto, pagamento fallito o limite del piano raggiunto
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Troppe richieste
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    CarrierError:
      description: Il corriere o il servizio esterno non risponde
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [unauthenticated, insufficient_scope, account_disabled, client_role_required, forbidden,
                     not_found, method_not_allowed, validation_failed, shipment_rejected, invalid_state,
                     subscription_required, subscription_payment_failed, plan_limit_reached, rate_limited,
                     carrier_error, http_error, internal_error]
            message: { type: string }
            details: {}

    Paginated:
      type: object
      properties:
        links:
          type: object
          properties:
            first: { type: [string, "null"] }
            last: { type: [string, "null"] }
            prev: { type: [string, "null"] }
            next: { type: [string, "null"] }
        meta:
          type: object
          properties:
            current_page: { type: integer }
            from: { type: [integer, "null"] }
            last_page: { type: integer }
            per_page: { type: integer }
            to: { type: [integer, "null"] }
            total: { type: integer }

    Money:
      type: object
      required: [amount]
      properties:
        amount: { type: number, minimum: 0 }
        currency: { type: string, minLength: 3, maxLength: 3, default: EUR }

    Weight:
      type: object
      required: [value]
      properties:
        value: { type: number, exclusiveMinimum: 0 }
        units: { type: string, enum: [KG, G], default: KG }

    Dimensions:
      type: object
      required: [length, width, height]
      properties:
        length: { type: number, exclusiveMinimum: 0 }
        width: { type: number, exclusiveMinimum: 0 }
        height: { type: number, exclusiveMinimum: 0 }
        units: { type: string, enum: [CM, MM], default: CM }

    Address:
      type: object
      required: [street, city, postalCode, countryCode]
      properties:
        street: { type: string, maxLength: 150 }
        street2: { type: string, maxLength: 100 }
        city: { type: string, maxLength: 80 }
        province: { type: string, maxLength: 10, description: "2 lettere per l'Italia" }
        postalCode: { type: string, maxLength: 15 }
        countryCode: { type: string, minLength: 2, maxLength: 2, description: ISO 3166-1 alpha-2 }

    Party:
      type: object
      required: [name, address]
      properties:
        name: { type: string, maxLength: 100 }
        company: { type: string, maxLength: 100 }
        address: { $ref: "#/components/schemas/Address" }
        phone: { type: string, maxLength: 30 }
        email: { type: string, format: email }
        reference: { type: string, maxLength: 50 }

    Package:
      type: object
      required: [weight, dimensions]
      properties:
        weight: { $ref: "#/components/schemas/Weight" }
        dimensions: { $ref: "#/components/schemas/Dimensions" }
        description: { type: string }
        value: { type: number }

    StatusKey:
      type: string
      enum: [processing, shipped, in_transit, out_for_delivery, delivered, held_at_depot,
             returned_to_sender, undelivered, awaiting_instructions, awaiting_collection]

    Status:
      type: object
      properties:
        code: { type: integer, description: "0/1 lavorazione, 2 spedita, 3 transito, 4 in consegna, 5 consegnata, 6 giacenza, 7 reso, 8 non consegnata, 9 attesa istruzioni, 10 attesa ritiro" }
        key: { $ref: "#/components/schemas/StatusKey" }
        label: { type: string }

    Carrier:
      type: object
      properties:
        code: { type: string, example: fedex }
        name: { type: string }
        contractCode: { type: string }
        contractName: { type: string }

    CarrierWithContracts:
      type: object
      properties:
        carrierCode: { type: string }
        carrierName: { type: string }
        canManageStocks: { type: boolean }
        canRequestPickup: { type: boolean }
        contracts:
          type: array
          items:
            type: object
            properties:
              contractCode: { type: string, description: "cifrato per i contratti secondari (L1)" }
              contractName: { type: string }
              level: { type: integer }
              slave: { type: boolean }
              paused: { type: boolean }
              multiParcel: { type: boolean }
              cashOnDeliveryMethods:
                type: object
                additionalProperties: { type: string }
        services:
          type: array
          items:
            type: object
            properties:
              serviceId: { type: integer }
              serviceCode: { type: string }
              serviceName: { type: string }

    ShipmentOptions:
      type: object
      properties:
        notes: { type: string, maxLength: 255 }
        contentDescription: { type: string, maxLength: 255 }
        labelFormat: { type: string, enum: [PDF, ZPL] }
        pickupFromAddress: { type: boolean }
        accessoryServices: { type: array, items: { type: integer }, description: serviceId da /carriers }
        cod:
          allOf:
            - $ref: "#/components/schemas/Money"
            - type: object
              properties:
                method: { type: string, description: chiave di cashOnDeliveryMethods }
                type: { type: string, description: codice interno del corriere (alternativa a method) }
        insurance: { $ref: "#/components/schemas/Money" }

    CustomsParty:
      type: object
      properties:
        eori: { type: [string, "null"] }
        vatNumber: { type: [string, "null"] }
        taxId: { type: [string, "null"] }

    Commodity:
      type: object
      required: [description, quantity, unitValue, weight, countryOfManufacture]
      properties:
        description: { type: string, maxLength: 255 }
        harmonizedCode: { type: [string, "null"], pattern: "^\\d{6,10}$" }
        quantity: { type: integer, minimum: 1 }
        quantityUnits: { type: string, enum: [PCS, PR, DOZ, KG, L, M, M2, M3, SET], default: PCS }
        unitValue: { $ref: "#/components/schemas/Money" }
        totalValue: { $ref: "#/components/schemas/Money" }
        weight: { $ref: "#/components/schemas/Weight" }
        countryOfManufacture: { type: string, minLength: 2, maxLength: 2 }
        midCode: { type: [string, "null"] }
        sku: { type: [string, "null"] }

    CustomsDetail:
      type: object
      required: [customsValue, commodities]
      properties:
        customsValue: { $ref: "#/components/schemas/Money" }
        incoterm: { type: string, enum: [EXW, FCA, CPT, CIP, DAP, DPU, DDP, FOB, CFR, CIF], default: DAP }
        dutiesPayment: { type: string, enum: [SENDER, RECIPIENT, THIRD_PARTY], default: RECIPIENT }
        contentType: { type: string, enum: [MERCHANDISE, GIFT, SAMPLE, DOCUMENTS, RETURN, REPAIR, PERSONAL_EFFECTS], default: MERCHANDISE }
        exportReason: { type: string, enum: [SALE, GIFT, SAMPLE, RETURN, REPAIR, PERSONAL], default: SALE }
        invoiceNumber: { type: [string, "null"] }
        invoiceDate: { type: [string, "null"], format: date }
        exporter: { $ref: "#/components/schemas/CustomsParty" }
        importer: { $ref: "#/components/schemas/CustomsParty" }
        commodities:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/Commodity" }

    ShipmentRequest:
      type: object
      required: [recipient, packages]
      properties:
        reference: { type: string, maxLength: 100, description: riferimento ordine del cliente }
        carrierCode: { type: string }
        serviceType: { type: string, description: "contractCode; omesso = primo contratto del preventivo" }
        shipper: { $ref: "#/components/schemas/Party" }
        recipient: { $ref: "#/components/schemas/Party" }
        packages:
          type: array
          minItems: 1
          maxItems: 50
          items: { $ref: "#/components/schemas/Package" }
        options: { $ref: "#/components/schemas/ShipmentOptions" }
        customsDetail: { $ref: "#/components/schemas/CustomsDetail" }

    RatesRequest:
      type: object
      required: [recipient, packages]
      properties:
        shipper:
          type: object
          properties:
            address: { $ref: "#/components/schemas/Address" }
        recipient:
          type: object
          required: [address]
          properties:
            address:
              type: object
              required: [postalCode, countryCode]
              properties:
                city: { type: string }
                province: { type: string }
                postalCode: { type: string }
                countryCode: { type: string }
        packages:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/Package" }
        options:
          type: object
          properties:
            pickupFromAddress: { type: boolean }
            cod: { $ref: "#/components/schemas/Money" }
            insurance: { $ref: "#/components/schemas/Money" }

    Rate:
      type: object
      properties:
        carrierCode: { type: string }
        carrierName: { type: string }
        contractCode: { type: string }
        contractName: { type: string }
        level: { type: integer }
        slave: { type: boolean }
        price:
          type: object
          properties:
            total: { type: number }
            currency: { type: string }
            breakdown:
              type: object
              properties:
                weight: { type: number }
                fuel: { type: number }
                insurance: { type: number }
                cod: { type: number }
                services: { type: number }
                extra: { type: number }
                pickup: { type: number }
        billableWeight: { $ref: "#/components/schemas/Weight" }
        volumetricWeight: { $ref: "#/components/schemas/Weight" }
        zone: { type: [string, "null"] }
        cashOnDeliveryMethods:
          type: object
          additionalProperties: { type: string }
        services:
          type: array
          items:
            type: object
            properties:
              serviceId: { type: integer }
              serviceName: { type: string }
        logoUrl: { type: [string, "null"] }

    Label:
      type: object
      properties:
        format: { type: string, enum: [PDF, ZPL], description: formato nativo dell'etichetta salvata }
        pdf: { type: [string, "null"], description: PDF in base64 }
        zpl: { type: [string, "null"], description: ZPL in base64 }

    Shipment:
      type: object
      properties:
        id: { type: integer }
        trackingNumber: { type: string }
        returnTrackingNumber: { type: [string, "null"] }
        shipmentKey: { type: [string, "null"] }
        reference: { type: [string, "null"] }
        bda: { type: [string, "null"] }
        status: { $ref: "#/components/schemas/Status" }
        carrier: { $ref: "#/components/schemas/Carrier" }
        shipper: { $ref: "#/components/schemas/Party" }
        recipient: { $ref: "#/components/schemas/Party" }
        packages:
          type: array
          items:
            type: object
            properties:
              number: { type: integer }
              trackingNumber: { type: string }
              incrementId: { type: [string, "null"], description: identificativo del collo assegnato dal corriere }
              reference: { type: [string, "null"] }
              weight: { $ref: "#/components/schemas/Weight" }
              dimensions: { $ref: "#/components/schemas/Dimensions" }
        parcels: { type: integer }
        weight: { $ref: "#/components/schemas/Weight" }
        volumetricWeight: { $ref: "#/components/schemas/Weight" }
        billableWeight: { $ref: "#/components/schemas/Weight" }
        cod:
          type: [object, "null"]
          properties:
            amount: { type: number }
            currency: { type: string }
            type: { type: [string, "null"] }
            status: { type: integer, description: "0 in attesa, 1 in lavorazione, 2 liquidato" }
        insurance:
          oneOf:
            - $ref: "#/components/schemas/Money"
            - type: "null"
        cost:
          type: object
          properties:
            total: { type: number }
            currency: { type: string }
            breakdown:
              type: object
              properties:
                weight: { type: number }
                insurance: { type: number }
                cod: { type: number }
                services: { type: number }
                extra: { type: number }
        accessoryServices: { type: array, items: { type: integer } }
        notes: { type: [string, "null"] }
        contentDescription: { type: [string, "null"] }
        customsDetail:
          oneOf:
            - $ref: "#/components/schemas/CustomsDetail"
            - type: "null"
        labelFormat: { type: string }
        deliveryDepot: { type: [string, "null"] }
        manifestId: { type: [integer, "null"] }
        warning: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        shippedAt: { type: [string, "null"], format: date-time }
        collectedAt: { type: [string, "null"], format: date-time }
        deliveredAt: { type: [string, "null"], format: date-time }
        trackingUpdatedAt: { type: [string, "null"], format: date-time }

    Tracking:
      type: object
      properties:
        trackingNumber: { type: string }
        reference: { type: [string, "null"] }
        carrier: { $ref: "#/components/schemas/Carrier" }
        status: { $ref: "#/components/schemas/Status" }
        events:
          type: array
          description: dal più recente; l'ultimo è la generazione della spedizione
          items:
            type: object
            properties:
              timestamp: { type: [string, "null"], format: date-time }
              description: { type: [string, "null"] }
              location: { type: [string, "null"] }
        deliveredAt: { type: [string, "null"], format: date-time }
        trackingUpdatedAt: { type: [string, "null"], format: date-time }

    PickupRequest:
      type: object
      required: [serviceType, pickupDate, shipper, packages]
      properties:
        serviceType: { type: string, description: contractCode }
        shipmentId: { type: string, description: LDV di riferimento }
        pickupDate: { type: string, format: date }
        pickupTime: { type: string, description: "es. AM, PM, AMPM", default: AMPM }
        instructions: { type: string }
        paymentType: { type: string }
        cod: { $ref: "#/components/schemas/Money" }
        shipper: { $ref: "#/components/schemas/Party", description: indirizzo di ritiro }
        recipient: { $ref: "#/components/schemas/Party", description: "default: sede operativa del cliente" }
        packages:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/Package" }

    Pickup:
      type: object
      properties:
        id: { type: integer }
        pickupId: { type: string }
        shipmentId: { type: [string, "null"] }
        carrier: { $ref: "#/components/schemas/Carrier" }
        pickupDate: { type: string, format: date }
        pickupTime: { type: [string, "null"] }
        paymentType: { type: [string, "null"] }
        shipper: { $ref: "#/components/schemas/Party" }
        parcels: { type: integer }
        weight: { $ref: "#/components/schemas/Weight" }
        notes: { type: [string, "null"] }
        cost: { $ref: "#/components/schemas/Money" }
        createdAt: { type: string, format: date-time }

    Manifest:
      type: object
      properties:
        id: { type: integer }
        carrier: { $ref: "#/components/schemas/Carrier" }
        totalShipments: { type: integer }
        totalCost: { type: number }
        createdAt: { type: string, format: date-time }
        pdf: { type: string, description: base64 }

    StockActionRequest:
      type: object
      required: [action]
      properties:
        action: { type: string, enum: [RETRY, NEWADDRESS, RETURN, DESTROY, ONSITECOLLECTION] }
        scheduledAt: { type: string, format: date }
        notes: { type: string }
        cod:
          type: object
          properties:
            oldAmount: { type: number }
            newAmount: { type: number }
        newAddress:
          type: object
          description: obbligatorio per NEWADDRESS
          required: [name, street, city, province, postalCode]
          properties:
            name: { type: string }
            street: { type: string }
            city: { type: string }
            province: { type: string }
            postalCode: { type: string }
            countryCode: { type: string, default: IT }
            phone: { type: string }
            notes: { type: string }

    Stock:
      type: object
      properties:
        id: { type: integer }
        trackingNumber: { type: string }
        shipmentId: { type: integer }
        dossierId: { type: [string, "null"] }
        status:
          type: object
          properties:
            code: { type: integer }
            key: { type: string, enum: [open, instructed, confirmed, closed] }
            label: { type: string }
        carrier: { $ref: "#/components/schemas/Carrier" }
        notes: { type: [string, "null"] }
        deposit:
          type: object
          properties:
            status: { type: [string, "null"] }
            releasable: { type: [boolean, "null"] }
            startDate: { type: [string, "null"], format: date-time }
            endDate: { type: [string, "null"], format: date-time }
            motivationId: { type: [string, "null"] }
        cod:
          type: object
          properties:
            oldAmount: { type: [number, "null"] }
            newAmount: { type: [number, "null"] }
        totalPrice: { type: number }
        lastAction:
          type: [object, "null"]
          properties:
            action: { type: [string, "null"] }
            actionCode: { type: integer }
            label: { type: [string, "null"] }
            scheduledAt: { type: [string, "null"], format: date-time }
            confirmed: { type: boolean }
            notes: { type: [string, "null"] }
            createdAt: { type: [string, "null"], format: date-time }
        releasedTrackingNumber: { type: [string, "null"] }
        openedAt: { type: [string, "null"], format: date-time }
        releasedAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }

    Webhook:
      type: object
      properties:
        id: { type: integer }
        url: { type: string, format: uri }
        description: { type: [string, "null"] }
        events: { type: array, items: { type: string } }
        isActive: { type: boolean }
        secret: { type: string, description: solo alla creazione }
        createdAt: { type: string, format: date-time }

    WebhookDelivery:
      type: object
      properties:
        id: { type: integer }
        event: { type: string }
        status: { type: string, enum: [success, failed] }
        responseCode: { type: [integer, "null"] }
        responseBody: { type: [string, "null"] }
        payload: {}
        createdAt: { type: string, format: date-time }
