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, ordini, 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.

    Dati personali: solo le app Spedisci.online ricevono i dati delle persone a cui sono indirizzati
    spedizioni e ordini. Un'integrazione esterna vede del destinatario solo `address{city, province,
    countryCode}` (schema `Destination`), senza `notes`/`warning`/`error.message` (restano `hasWarning`,
    `hasError`) e senza `importer` nella dichiarazione doganale; i filtri `recipientName` e `postalCode`
    rispondono 422 e `q` sugli ordini cerca solo numeri d'ordine e LDV. Questo documento descrive la
    vista delle integrazioni esterne.
    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: Auth
  - name: Rates
  - name: Shipments
  - name: Tracking
  - name: Pickups
  - name: Manifests
  - name: Stocks
    description: Giacenze
  - name: Delivery points
  - name: Customs
  - name: Webhooks
  - name: Orders
    description: Ordini dei negozi collegati e inviati dal gestionale del cliente

paths:
  /logout:
    post:
      tags: [Auth]
      summary: Revoca il token della richiesta
      description: |
        Revoca l'access token usato per questa chiamata e i refresh token emessi con esso.
        Gli altri token dello stesso utente (altri dispositivi, altre App) restano validi.
      responses:
        "200":
          description: Token revocato
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked: { type: boolean, const: true }
        "401":
          $ref: "#/components/responses/Unauthenticated"

  /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: city, 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" }
                      returnLabel:
                        description: "solo con options.returnLabel=separate: l'etichetta di restituzione emessa con la spedizione (Poste Andata & Ritorno), null se non c'è"
                        oneOf:
                          - $ref: "#/components/schemas/ReturnLabel"
                          - type: "null"
        "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" }
        - { name: returnLabel, in: query, schema: { type: string, enum: [merged, separate] }, description: "merged (default): l'etichetta di restituzione emessa con la spedizione (Poste Andata & Ritorno) segue l'etichetta nello stesso file; separate: arriva a parte in returnLabel" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      trackingNumber: { type: string }
                      returnLabel:
                        description: solo con returnLabel=separate
                        oneOf:
                          - $ref: "#/components/schemas/ReturnLabel"
                          - type: "null"
                  - $ref: "#/components/schemas/Label"
        "404": { $ref: "#/components/responses/NotFound" }

  /shipments/{ldv}/return:
    get:
      tags: [Shipments]
      summary: La restituzione (Reverse Poste) della spedizione
      parameters:
        - { $ref: "#/components/parameters/ldv" }
        - { name: files, in: query, schema: { type: boolean }, description: "true: anche etichetta e codice 2D in returnLabel" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ShipmentReturn" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Shipments]
      summary: Prenota la restituzione Poste della spedizione
      description: Reverse Poste dal destinatario all'indirizzo di reso del cliente, su una spedizione Poste nazionale senza restituzione. Addebitata solo alla prima scansione di Poste.
      parameters:
        - { $ref: "#/components/parameters/ldv" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [channel]
              properties:
                channel: { type: string, enum: [APT000928, APT000979, APT000980, APT000981], description: "ritiro a domicilio, Punto Poste, Locker Punto Poste, Ufficio Postale" }
                paperless: { type: boolean, description: "codice 2D al posto dell'etichetta (solo Punto Poste e Ufficio Postale)" }
      responses:
        "201":
          description: Restituzione prenotata (con i file in returnLabel)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ShipmentReturn" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }

  /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 o Poste)
      description: |
        Con `type` restituisce l'elenco normalizzato dei punti di quel tipo: BRT `fermopoint` (negozio) e `brtlocker`
        (BRT Locker), vicini a CAP e città; Poste (`carrier=poste`) `puntoposte` (Punto Poste), `locker` (Punto Poste
        Locker), `ufficio` (ufficio postale), vicini al CAP. Il punto scelto si passa alla creazione della spedizione
        in `options.deliveryPoint`. Richiede un contratto del corriere che consegna a quel tipo di punto.
        Senza `type` restituisce la risposta del corriere BRT com'è.
      parameters:
        - { name: carrier, in: query, schema: { type: string, enum: [brt, poste], default: brt } }
        - { name: postalCode, in: query, required: true, schema: { type: string } }
        - { name: city, in: query, schema: { type: string }, description: obbligatorio per BRT }
        - { name: address, in: query, schema: { type: string } }
        - { name: type, in: query, schema: { type: string, enum: [puntoposte, locker, ufficio, fermopoint, brtlocker] }, description: obbligatorio per Poste; per BRT attiva l'elenco normalizzato }
      responses:
        "200":
          description: Con `type` un elenco di DeliveryPoint; senza, la risposta del corriere BRT
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    oneOf:
                      - type: array
                        items: { $ref: "#/components/schemas/DeliveryPoint" }
                      - {}
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "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" }

  /orders:
    get:
      tags: [Orders]
      summary: Elenco ordini
      description: |
        Ordini importati dai negozi collegati (Amazon, eBay, Shopify, WooCommerce…) e inviati con `POST /orders`.
        Senza `from` copre gli ultimi 30 giorni per data ordine; con `toShip=true` gli ordini da spedire senza limite di data.
      parameters:
        - { name: storeId, in: query, schema: { type: integer } }
        - { name: from, in: query, schema: { type: string, format: date }, description: "data ordine; default: 30 giorni fa" }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: sourceId, in: query, schema: { type: integer } }
        - { name: marketplaceId, in: query, schema: { type: string } }
        - { name: shipped, in: query, schema: { type: boolean } }
        - { name: confirmed, in: query, schema: { type: boolean } }
        - { name: archived, in: query, schema: { type: boolean, default: false } }
        - { name: hasError, in: query, schema: { type: boolean } }
        - { name: toShip, in: query, schema: { type: boolean }, description: "non spediti e non archiviati, senza limite di data" }
        - { name: trackingNumber, in: query, schema: { type: string } }
        - { name: countryCode, in: query, schema: { type: string, minLength: 2, maxLength: 2 } }
        - { name: isPrime, in: query, schema: { type: boolean } }
        - { name: isBusiness, in: query, schema: { type: boolean } }
        - { name: sku, in: query, schema: { type: string }, description: prefisso dello SKU di un articolo }
        - { name: q, in: query, schema: { type: string }, description: "cerca nel numero d'ordine e nella LDV" }
        - { name: sort, in: query, schema: { type: string, enum: [orderDate, -orderDate, latestShipDate, -latestShipDate, createdAt, -createdAt, id, -id], default: -orderDate } }
        - { $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/Order" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
    post:
      tags: [Orders]
      summary: Invio ordini (fino a 100 per chiamata)
      description: |
        Gli ordini compaiono nella pagina Ordini da API della piattaforma, come un import da file CSV, e da lì si
        spediscono. Valori predefiniti: 1 collo, 1 kg, Italia, data odierna, ragione sociale del cliente come
        riferimento mittente. Il lotto è validato per intero: un ordine non valido respinge la chiamata e le chiavi
        di `details` indicano quale (`orders.3.recipient.address.postalCode`). Un `orderNumber` già inviato non viene
        importato di nuovo ma elencato in `skipped`, così un lotto può essere ripetuto senza creare doppioni.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [orders]
              properties:
                saveToAddressBook: { type: boolean, default: false, description: salva i destinatari in rubrica }
                orders:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items: { $ref: "#/components/schemas/OrderRequest" }
      responses:
        "201":
          description: Almeno un ordine creato
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrdersCreated" }
        "200":
          description: Tutti gli ordini erano già stati inviati
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrdersCreated" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /orders/summary:
    get:
      tags: [Orders]
      summary: Contatori degli ordini, complessivi e per negozio
      parameters:
        - { name: storeId, in: query, schema: { type: integer } }
        - { name: from, in: query, schema: { type: string, format: date }, description: "data ordine; senza from/to conta tutti gli ordini" }
        - { name: to, in: query, schema: { type: string, format: date } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrdersSummary" }

  /orders/{id}:
    get:
      tags: [Orders]
      summary: Dettaglio ordine con articoli, colli e spedizioni create
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrderDetail" }
        "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 }

    Destination:
      type: object
      description: dove va la spedizione, senza i dati della persona
      properties:
        address:
          type: object
          properties:
            city: { type: [string, "null"] }
            province: { type: [string, "null"] }
            countryCode: { type: [string, "null"], description: ISO 3166-1 alpha-2 }

    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 }

    ShipmentOptions:
      type: object
      properties:
        notes: { type: string, maxLength: 255 }
        contentDescription: { type: string, maxLength: 255 }
        labelFormat: { type: string, enum: [PDF, ZPL] }
        returnLabel: { type: string, enum: [merged, separate], description: "dove arriva l'etichetta di restituzione emessa con la spedizione (Poste Andata & Ritorno): merged (default) nello stesso file dell'etichetta, separate a parte in returnLabel" }
        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" }
        deliveryPoint:
          type: object
          description: |
            Consegna presso un punto del corriere invece che al domicilio, solo Italia. Poste (Poste Delivery Business
            Express/Standard): contrassegno e più colli solo con `ufficio`; niente Andata e Ritorno; sull'etichetta va
            l'indirizzo del punto, nome e recapiti restano quelli del destinatario. BRT (contratti diretti): senza
            contrassegno; l'etichetta resta intestata al destinatario e BRT consegna al punto.
          required: [type, code]
          properties:
            type: { type: string, enum: [puntoposte, locker, ufficio, fermopoint, brtlocker] }
            code: { type: string, description: "`code` del punto restituito da GET /delivery-points?type=" }
            postalCode: { type: string, description: "CAP usato nella ricerca; default il CAP del destinatario" }
            city: { type: string, description: "città usata nella ricerca (BRT); default quella del destinatario" }

    DeliveryPoint:
      type: object
      properties:
        carrier: { type: string, enum: [poste, brt] }
        type: { type: string, enum: [puntoposte, locker, ufficio, fermopoint, brtlocker] }
        code: { type: string }
        name: { type: string }
        address: { type: string }
        postalCode: { type: string }
        city: { type: string }
        province: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        lat: { type: [number, "null"] }
        lng: { type: [number, "null"] }
        hours:
          type: [object, "null"]
          description: Poste
          properties:
            weekdays: { type: [object, "null"], properties: { open: { type: string }, close: { type: string } } }
            saturday: { type: [object, "null"], properties: { open: { type: string }, close: { type: string } } }
        hoursText: { type: [string, "null"], description: "BRT: orari su una riga, es. `Lun-Ven 09:00-13:00, 15:30-19:30 · Sab 09:00-13:00`" }
        distance: { type: [integer, "null"], description: "BRT: metri dal luogo cercato" }
        saturation: { type: [integer, "null"], description: Poste, percentuale di saturazione del punto }

    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" }
        warehouseId: { type: integer, description: "id del punto di ritiro da cui parte la spedizione (Impostazioni → Punti di ritiro); omesso = punto predefinito. Il mittente, se non indicato, e l'indirizzo di reso vengono presi dal punto" }
        shipper: { $ref: "#/components/schemas/Party", description: "omesso = origine del punto di ritiro" }
        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:
        warehouseId: { type: integer, description: "id del punto di ritiro da cui parte la spedizione (Impostazioni → Punti di ritiro); omesso = punto predefinito. Con pickupFromAddress, un punto con ritiro gratuito approvato quota il ritiro a zero" }
        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 }

    ReturnLabel:
      type: object
      description: l'etichetta di restituzione emessa con la spedizione, nel formato in cui il corriere l'ha emessa
      properties:
        trackingNumber: { type: string, description: LDV della restituzione (returnTrackingNumber) }
        pdf: { type: [string, "null"], description: PDF in base64 }
        zpl: { type: [string, "null"], description: ZPL in base64 }
        code: { type: [string, "null"], description: "codice 2D (gif) in base64 di una restituzione senza stampa" }

    ShipmentReturn:
      type: object
      properties:
        trackingNumber: { type: string }
        returnTrackingNumber: { type: string }
        status: { type: string, enum: [pending, billed, failed, issued], description: "issued: un ritorno di Andata & Ritorno emesso prima che i ritorni venissero seguiti" }
        automatic: { type: boolean, description: prenotata dalla piattaforma alla creazione della spedizione }
        roundTrip: { type: boolean, description: "il ritorno di un'Andata & Ritorno: pagato con l'andata, seguito ma mai addebitato" }
        channel: { type: [string, "null"] }
        paperless: { type: boolean }
        estimatedPrice: { type: [number, "null"], description: addebitato solo se usata }
        firstEventAt: { type: [string, "null"], format: date-time }
        lastCheckedAt: { type: [string, "null"], format: date-time }
        billedShipment:
          oneOf:
            - type: object
              properties:
                id: { type: integer }
                trackingNumber: { type: string }
                cost: { type: number }
                manifestId: { type: [integer, "null"] }
            - type: "null"
        error: { type: [string, "null"] }
        warning: { type: [string, "null"] }
        returnLabel:
          oneOf:
            - $ref: "#/components/schemas/ReturnLabel"
            - type: "null"

    Shipment:
      type: object
      properties:
        id: { type: integer }
        trackingNumber: { type: string }
        returnTrackingNumber: { type: [string, "null"], description: "LDV della restituzione emessa con la spedizione (Poste Andata & Ritorno), se c'è" }
        shipmentKey: { type: [string, "null"] }
        reference: { type: [string, "null"] }
        bda: { type: [string, "null"] }
        status: { $ref: "#/components/schemas/Status" }
        carrier: { $ref: "#/components/schemas/Carrier" }
        warehouseId: { type: [integer, "null"], description: punto di ritiro da cui e partita }
        shipper: { $ref: "#/components/schemas/Party", description: "l'indirizzo del cliente stesso" }
        recipient: { $ref: "#/components/schemas/Destination" }
        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 } }
        contentDescription: { type: [string, "null"] }
        customsDetail:
          description: senza il blocco importer
          oneOf:
            - $ref: "#/components/schemas/CustomsDetail"
            - type: "null"
        deliveryPoint:
          oneOf:
            - $ref: "#/components/schemas/DeliveryPoint"
            - type: "null"
        labelFormat: { type: string }
        deliveryDepot: { type: [string, "null"] }
        manifestId: { type: [integer, "null"] }
        hasWarning: { type: boolean, description: "il corriere ha segnalato un avviso sulla spedizione" }
        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, 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" }
        warehouseId: { type: integer, description: "id del punto di ritiro presso cui prenotare (Impostazioni → Punti di ritiro); omesso = punto predefinito. Un punto con ritiro gratuito approvato non viene addebitato" }
        shipper: { $ref: "#/components/schemas/Party", description: "indirizzo di ritiro; obbligatorio solo senza warehouseId" }
        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"] }
        warehouseId: { type: [integer, "null"], description: punto di ritiro presso cui e prenotato }
        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 }

    OrderRequest:
      type: object
      required: [recipient]
      properties:
        orderNumber: { type: string, maxLength: 50, description: "numero dell'ordine nel gestionale; unico per cliente, rende la chiamata ripetibile" }
        orderDate: { type: string, format: date, description: default oggi }
        recipient:
          type: object
          required: [name, address]
          properties:
            name: { type: string, maxLength: 100 }
            phone: { type: string, maxLength: 30 }
            email: { type: string, format: email }
            reference: { type: string, maxLength: 50 }
            address:
              type: object
              required: [street, city, postalCode]
              properties:
                street: { type: string, maxLength: 150 }
                city: { type: string, maxLength: 80 }
                province: { type: string, maxLength: 50, description: "sigla o nome per esteso della provincia italiana" }
                postalCode: { type: string, maxLength: 15 }
                countryCode: { type: string, minLength: 2, maxLength: 2, default: IT, description: ISO 3166-1 alpha-2 }
        senderReference: { type: string, maxLength: 50, description: default la ragione sociale del cliente }
        parcels: { type: integer, minimum: 1, maximum: 50, default: 1 }
        weight: { $ref: "#/components/schemas/Weight", description: default 1 kg }
        cod: { $ref: "#/components/schemas/Money", description: contrassegno }
        insurance: { $ref: "#/components/schemas/Money" }
        total: { type: number, minimum: 0, description: "totale dell'ordine" }
        contentDescription: { type: string, maxLength: 255 }
        notes: { type: string, maxLength: 255 }
        items:
          type: array
          maxItems: 200
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku: { type: string, maxLength: 100 }
              quantity: { type: integer, minimum: 1 }
              name: { type: string, maxLength: 255, description: "default il titolo del prodotto con lo stesso SKU nel catalogo" }
              unitPrice: { type: number, minimum: 0 }

    Order:
      type: object
      properties:
        id: { type: integer }
        storeOrderId: { type: [string, "null"], description: numero dell'ordine nel negozio }
        orderNumber: { type: [string, "null"] }
        reference: { type: [string, "null"] }
        storeId: { type: [integer, "null"], description: "null per gli ordini inviati via API" }
        sourceId: { type: [integer, "null"], description: "99 per gli ordini inviati via API" }
        marketplaceId: { type: [string, "null"] }
        recipient: { $ref: "#/components/schemas/Destination" }
        senderReference: { type: [string, "null"] }
        parcels: { type: integer }
        weight: { $ref: "#/components/schemas/Weight" }
        cod: { oneOf: [{ $ref: "#/components/schemas/Money" }, { type: "null" }] }
        insurance: { oneOf: [{ $ref: "#/components/schemas/Money" }, { type: "null" }] }
        total: { type: [number, "null"] }
        subtotal: { type: [number, "null"] }
        shippingCost: { type: [number, "null"], description: spese di spedizione pagate dall'acquirente }
        contentDescription: { type: [string, "null"] }
        sku: { type: [string, "null"] }
        tags: { type: array, items: { type: string } }
        paymentStatus: { type: [string, "null"] }
        paymentGateway: { type: [string, "null"] }
        fulfillmentStatus: { type: [string, "null"] }
        isPrime: { type: boolean }
        isBusiness: { type: boolean }
        shipped: { type: boolean }
        confirmed: { type: boolean }
        archived: { type: boolean }
        trackingNumber: { type: [string, "null"] }
        hasError: { type: boolean, description: "l'ultimo tentativo di spedizione è fallito" }
        group:
          type: [object, "null"]
          properties:
            id: { type: integer }
            isPrimary: { type: boolean }
            orderIds: { type: array, items: { type: integer }, description: solo nel dettaglio }
        itemsCount: { type: integer }
        firstItem:
          type: [object, "null"]
          properties:
            sku: { type: [string, "null"] }
            name: { type: [string, "null"] }
            quantity: { type: integer }
            imageUrl: { type: [string, "null"] }
        orderDate: { type: [string, "null"], format: date-time }
        latestShipDate: { type: [string, "null"], format: date-time }
        createdAt: { type: [string, "null"], format: date-time }

    OrderDetail:
      allOf:
        - $ref: "#/components/schemas/Order"
        - type: object
          properties:
            items:
              type: array
              items:
                type: object
                properties:
                  id: { type: integer }
                  itemId: { type: [string, "null"] }
                  sku: { type: [string, "null"] }
                  asin: { type: [string, "null"] }
                  name: { type: [string, "null"] }
                  quantity: { type: integer }
                  unitPrice: { type: [number, "null"] }
                  total: { type: [number, "null"] }
                  imageUrl: { type: [string, "null"] }
                  variants: {}
            packs:
              type: array
              description: colli dichiarati dal negozio
              items:
                type: object
                properties:
                  number: { type: integer }
                  weight: { $ref: "#/components/schemas/Weight" }
                  dimensions: { $ref: "#/components/schemas/Dimensions" }
            shipments:
              type: array
              description: LDV già create per l'ordine, dalla più recente
              items:
                type: object
                properties:
                  id: { type: integer }
                  trackingNumber: { type: [string, "null"] }
                  status: { $ref: "#/components/schemas/Status" }
                  manifestId: { type: [integer, "null"] }
                  cost: { $ref: "#/components/schemas/Money" }
                  createdAt: { type: [string, "null"], format: date-time }

    OrdersSummaryCounters:
      type: object
      properties:
        total: { type: integer }
        toShip: { type: integer, description: non spediti e non archiviati }
        withError: { type: integer, description: da spedire con l'ultimo tentativo fallito }
        shipped: { type: integer }
        archived: { type: integer }
        prime: { type: integer, description: da spedire con Amazon Prime }

    OrdersSummary:
      allOf:
        - $ref: "#/components/schemas/OrdersSummaryCounters"
        - type: object
          properties:
            stores:
              type: array
              items:
                allOf:
                  - type: object
                    properties:
                      storeId: { type: [integer, "null"] }
                      sourceId: { type: [integer, "null"] }
                  - $ref: "#/components/schemas/OrdersSummaryCounters"

    OrdersCreated:
      type: object
      properties:
        created:
          type: array
          items: { $ref: "#/components/schemas/Order" }
        skipped:
          type: array
          description: ordini con un orderNumber già inviato, non importati di nuovo
          items:
            type: object
            properties:
              index: { type: integer, description: "posizione nell'array orders della richiesta" }
              orderNumber: { type: string }
              id: { type: integer, description: "id dell'ordine già presente" }
