# Stoccaggio e uscita

L’API di stoccaggio e uscita consente a un account cliente di un’azienda di magazzino di depositare merce in stoccaggio, pagare il periodo di stoccaggio e successivamente spedire i colli stoccati ai propri acquirenti. È destinata a commercianti e piattaforme che tengono le scorte presso un magazzino di terzi (3PL) e devono automatizzare dai propri sistemi la prenotazione dello stoccaggio, la consultazione delle giacenze e le spedizioni in uscita. Tutte le chiamate vengono eseguite come account cliente, mai come azienda di magazzino.

## 1. Cosa potete realizzare

Gli esempi di questa guida seguono un unico scenario. **Northwind Outdoor**, un venditore online stagionale di abbigliamento invernale, stocca le proprie scorte invernali presso il **Toronto Hub** (magazzino `7`) del suo 3PL dal 1° novembre 2026 al 31 marzo 2027. Quando un acquirente ordina un cartone di giacche imbottite, Northwind spedisce quel cartone dalle giacenze all’acquirente a Ottawa.

- **Prenotazione dello stoccaggio stagionale.** Il gestionale del venditore quota e prenota un periodo di stoccaggio per ogni cartone in entrata prima che la merce lasci il fornitore, e paga la tariffa di stoccaggio dal saldo del proprio account.
- **Consultazione delle giacenze in tempo reale.** Il negozio o l’ERP del venditore elenca i colli che il magazzino ha effettivamente ricevuto e che sono ancora disponibili per la spedizione, in modo che per l’evasione venga offerta solo merce realmente disponibile.
- **Evasione degli ordini dalle giacenze.** Quando un acquirente effettua un ordine, il sistema del venditore calcola il prezzo della spedizione in uscita, crea una richiesta di uscita per i colli stoccati, la paga e registra il numero di tracking per l’acquirente.
- **Monitoraggio e correzione dello stato.** Il sistema del venditore legge lo stato di ogni ordine di stoccaggio e di ogni uscita, segue la spedizione tramite il tracking pubblico e annulla un’uscita non più necessaria finché l’annullamento è ancora consentito.

## 2. Cosa copre questa guida

Usate questa guida quando la merce è già, o sarà, stoccata nel magazzino dell’azienda e la spedizione parte da quelle giacenze. Il flusso è: accesso → lettura della configurazione di stoccaggio → quotazione dello stoccaggio → creazione dell’ordine di stoccaggio → pagamento → elenco dei colli in giacenza → elenco dei servizi e stima dell’uscita → creazione dell’uscita → pagamento → lettura e tracking → webhook → annullamento.

Altre guide coprono altri casi:

- **Uniorder: un'API per ogni spedizione** — il punto di ingresso unico consigliato (`/api/v1/uniorder/...`) per le nuove integrazioni che prenotano consegne locali o etichette corriere. Uniorder **non** copre lo stoccaggio e l’uscita; gli ordini di stoccaggio e le uscite vengono creati solo tramite gli endpoint cliente descritti in questa guida.
- **Servizi di spedizione** — un cliente spedisce merce che non è in stoccaggio, utilizzando i servizi di spedizione dell’azienda.
- **Etichette corriere** — un’azienda acquista direttamente etichette corriere per i propri colli.
- **Ritiro e consegna (flotta propria)** — un’azienda prenota ritiri e consegne con la propria flotta.

## 3. Prima di iniziare

- **Tipo di account.** Un account **cliente** dell’azienda di magazzino (l’azienda che gestisce il magazzino è il fornitore del servizio). Il token di un account aziendale (client) non funziona sugli endpoint `/api/v1/customer/...`.
- **Autorizzazioni.** L’account cliente deve disporre dell’accesso API. Gli endpoint di stoccaggio richiedono inoltre la funzionalità di stoccaggio; gli endpoint di uscita richiedono che l’azienda abbia abilitato l’uscita (o il consolidamento) per questo cliente, altrimenti rispondono `403`.
- **Saldo.** I pagamenti di stoccaggio e di uscita vengono addebitati sul saldo dell’account cliente. Per un test, chiedete all’azienda di accreditare il saldo del cliente di prova.
- **Dati di test.** Un `id` di magazzino, almeno un `id` di imballaggio se i colli personalizzati non sono consentiti e almeno un servizio di spedizione attivo disponibile da quel magazzino. L’uscita funziona solo dopo che il magazzino ha **ricevuto** i colli stoccati; in un test, chiedete al personale del magazzino di ricevere l’ordine di stoccaggio di prova.
- **Gestione del token.** Effettuate l’accesso dal vostro server, conservate il token sul server e non inseritelo mai nel codice per browser o dispositivi mobili.
- **Segnaposto.** Sostituite `YOUR_HOST` con l’host della vostra piattaforma e `ACCESS_TOKEN` con il token del passo 4.

## 4. Accesso come cliente

Ogni chiamata successiva è autorizzata con un bearer token del cliente. La vostra integrazione effettua l’accesso una volta, conserva il token lato server e lo rinnova prima di `expires_at`.

**REST:** `POST /api/v1/user/customer/login` — [Manuale REST](/api/documentation#/paths/v1-user-customer-login/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/customer/login \
  -H "Content-Type: application/json" \
  -d '{"email":"ops@northwind-outdoor.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2027-09-28 10:15:00",
  "expires_timestamp": 1822040100,
  "name": "Northwind Outdoor"
}
```

- `access_token` — inviatelo in ogni richiesta come l’intestazione seguente.
- `expires_at` / `expires_timestamp` — effettuate di nuovo l’accesso prima di questo momento.

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL usa la stessa intestazione su `POST /api/graphql`.

**Verifica:** il login restituisce `access_token`. Le richieste successive senza questo token restituiscono `401`.

## 5. Leggere la configurazione dello stoccaggio

Il pacchetto di configurazione elenca i magazzini che il cliente può utilizzare, il catalogo degli imballaggi, le unità e i supplementi. La vostra integrazione lo legge una volta per sessione per scegliere il magazzino e comporre righe di colli valide.

**REST:** `GET /api/v1/customer/storage-orders/config` — [Manuale REST](/api/documentation#/paths/v1-customer-storage-orders-config/get)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/config \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "warehouses": [
      { "id": 7, "name": "Toronto Hub", "address": "10 Main St", "city": "Toronto", "province": "ON", "postcode": "M5V 2T6" }
    ],
    "packagings": [
      { "id": 1, "name": "Large Carton", "type": "Box", "length": 60, "width": 40, "height": 40, "dimension_unit": 2 }
    ],
    "dimension_units": { "1": { "name": "dimension_mm" }, "2": { "name": "dimension_cm" }, "3": { "name": "dimension_m" }, "4": { "name": "dimension_inch" } },
    "weight_units": { "1": { "name": "weight_g" }, "2": { "name": "weight_kg" }, "3": { "name": "weight_oz" }, "4": { "name": "weight_lb" } },
    "allow_custom_package": true,
    "surcharges": [],
    "form_bindings": []
  }
}
```

- `warehouses[].id` — il `warehouse_id` per ogni chiamata successiva.
- `allow_custom_package` — quando è `false`, ogni articolo in stoccaggio deve riportare un `packaging_id` da `packagings[]`; quando è `true`, gli articoli possono essere descritti solo dalle dimensioni.
- `dimension_units` / `weight_units` — i codici interi usati nelle righe dei colli (`2` = cm, `2` = kg).
- `form_bindings` — i moduli che l’azienda richiede su un ordine di stoccaggio; inviate le relative risposte come `form_data` nel passo 7.

**GraphQL:** `customerStorageOrderConfig` ([Manuale GraphQL](/api/graphql/documentation#/customer/customerStorageOrderConfig))

```graphql
query {
  customerStorageOrderConfig
}
```

**Verifica:** avete annotato un `id` di magazzino e, se il catalogo non è vuoto, un `id` di imballaggio.

## 6. Quotare il periodo di stoccaggio

Il preventivo calcola il prezzo del periodo di stoccaggio per i colli previsti prima di qualsiasi prenotazione. La vostra integrazione mostra o controlla questo prezzo, quindi crea l’ordine con gli stessi dati.

**REST:** `POST /api/v1/customer/storage-orders/calculate-price` — [Manuale REST](/api/documentation#/paths/v1-customer-storage-orders-calculate-price/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/calculate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-11-01",
    "end_date": "2027-03-31",
    "items": [{
      "qty": 2,
      "length": 60,
      "width": 40,
      "height": 40,
      "dimension_unit": 2,
      "weight": 14,
      "weight_unit": 2
    }]
  }'
```

```json
{
  "success": true,
  "price": {
    "total_price": "186.45",
    "currency": "CAD",
    "shipping_price": 186.45
  }
}
```

- `success` — `true` quando è stato calcolato un prezzo.
- `price.total_price` / `price.currency` — il prezzo dello stoccaggio per il periodo, imposte incluse.
- `promotion` — presente solo quando si applica una promozione.

**Verifica:** `success` o `result` è true e avete un prezzo. `warehouse_id` / date mancanti restituiscono `400`.

## 7. Creare l’ordine di stoccaggio

L’ordine di stoccaggio preannuncia al magazzino i colli in entrata e fissa il periodo di stoccaggio. La vostra integrazione conserva l’id restituito, necessario per pagare, consultare e annullare l’ordine.

**REST:** `POST /api/v1/customer/storage-orders` — [Manuale REST](/api/documentation#/paths/v1-customer-storage-orders/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: northwind-winter-2026-po-4471" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-11-01",
    "end_date": "2027-03-31",
    "notes": "Winter 2026 stock, PO 4471",
    "items": [{
      "description": "Insulated jackets, carton of 12",
      "qty": 2,
      "length": 60,
      "width": 40,
      "height": 40,
      "dimension_unit": 2,
      "weight": 14,
      "weight_unit": 2,
      "value": 1800
    }]
  }'
```

```json
{
  "result": true,
  "message": "Storage order created",
  "data": { "id": 1024, "status": "pending payment" }
}
```

- `data.id` — l’id dell’ordine di stoccaggio. Conservatelo insieme al vostro ordine d’acquisto.
- `data.status` — `pending payment` finché l’ordine non è pagato.
- `Idempotency-Key` — ricavatela da un vostro id stabile. Una chiave ripetuta con lo stesso corpo restituisce di nuovo la prima risposta (`replayed: true`); la stessa chiave con un corpo diverso viene rifiutata con `409 IDEMPOTENCY_CONFLICT`.
- Campi obbligatori: `warehouse_id`, `start_date`, `end_date` (successiva a `start_date`) e `items[]` con `qty`, `length`, `width`, `height`, `dimension_unit`. Aggiungete `items[].packaging_id` quando `allow_custom_package` è `false`.

**Verifica:** la risposta contiene `data.id`. Conservate quell’id dell’ordine di stoccaggio.

## 8. Pagare lo stoccaggio

Il pagamento conferma l’ordine di stoccaggio. La vostra integrazione può prima leggere l’importo dovuto, quindi paga dal saldo del cliente.

**REST:** `GET /api/v1/customer/storage-orders/{id}/payment-info` — [Manuale REST](/api/documentation#/paths/v1-customer-storage-orders-id--payment-info/get) (facoltativo)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/1024/payment-info \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "success": true,
  "result": true,
  "data": {
    "order_id": 1024,
    "currency": "CAD",
    "total_price": "186.45",
    "paid_amount": "0.00",
    "remaining_balance": "186.45",
    "user_balance": "500.00",
    "has_sufficient_balance": true,
    "minimum_payment": "186.45"
  }
}
```

**REST:** `POST /api/v1/customer/storage-orders/{id}/pay` — [Manuale REST](/api/documentation#/paths/v1-customer-storage-orders-id--pay/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/1024/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"payment_type": "full_balance"}'
```

```json
{
  "success": true,
  "result": true,
  "message": "Payment of $186.45 processed successfully. Your storage order is now confirmed.",
  "new_balance": 313.55,
  "paid_amount": 186.45,
  "charge_amount": 186.45,
  "is_fully_paid": true
}
```

- `payment_type` — `full_balance` (predefinito, paga l’intero importo residuo), `minimum_payment` (paga il minimo richiesto dall’azienda) oppure `custom` insieme a `custom_amount`.
- `is_fully_paid` — `true` quando non resta nulla da pagare.
- Un saldo insufficiente restituisce `400` con `customer_balance`; ricaricate il saldo, quindi riprovate.

Consultate l’ordine per confermarne lo stato e, in seguito, quali colli il magazzino ha ricevuto.

**REST:** `GET /api/v1/customer/storage-orders/{id}` — [Manuale REST](/api/documentation#/paths/v1-customer-storage-orders-id/get)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/1024 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "id": 1024,
    "status": "confirmed",
    "can_cancel": true,
    "store_from": "2026-11-01",
    "store_to": "2027-03-31",
    "warehouse_id": 7,
    "packages": [
      { "id": 5001, "description": "Insulated jackets, carton of 12", "quantity": 1, "received": false },
      { "id": 5002, "description": "Insulated jackets, carton of 12", "quantity": 1, "received": false }
    ],
    "total_price": 186.45,
    "currency": "CAD"
  }
}
```

- `status` — `confirmed` dopo il pagamento; in seguito `partial received` / `storage in progress` man mano che la merce arriva.
- `packages[].received` — `true` quando il magazzino ha ricevuto quel collo.
- `can_cancel` — indica se l’ordine di stoccaggio può ancora essere annullato.

**GraphQL:** `customerStorageOrderShow` ([Manuale GraphQL](/api/graphql/documentation#/customer/customerStorageOrderShow)); l’elenco di tutti gli ordini di stoccaggio è `customerStorageOrders` ([Manuale GraphQL](/api/graphql/documentation#/customer/customerStorageOrders)).

```graphql
query {
  customerStorageOrderShow(id: 1024) {
    result
    data {
      id
      status
      can_cancel
      packages { id description received }
    }
  }
}
```

**Verifica:** l’ordine di stoccaggio è pagato / confermato. Un `400` con `customer_balance` significa che occorre ricaricare il saldo e poi riprovare.

L’uscita descritta di seguito funziona solo dopo che i colli sono stati **ricevuti** nel magazzino. Per un test, attendete che il personale (o una ricezione di prova) li abbia contrassegnati come ricevuti, quindi proseguite.

## 9. Elencare gli articoli ancora in giacenza

Questo elenco rappresenta le giacenze che la vostra integrazione può spedire. Contiene solo i colli che il magazzino ha ricevuto e che non sono già bloccati su un’altra uscita.

**REST:** `GET /api/v1/customer/shipout-orders/available-items` — [Manuale REST](/api/documentation#/paths/v1-customer-shipout-orders-available-items/get)

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/available-items?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "warehouses": [{ "id": 7, "name": "Toronto Hub", "available_count": 2 }],
    "storage_orders": [{
      "id": 1024,
      "warehouse_id": 7,
      "packages": [
        { "id": 5001, "description": "Insulated jackets, carton of 12", "weight": 14, "weight_unit": 2, "length": 60, "width": 40, "height": 40, "dimension_unit": 2, "value": 1800 },
        { "id": 5002, "description": "Insulated jackets, carton of 12", "weight": 14, "weight_unit": 2, "length": 60, "width": 40, "height": 40, "dimension_unit": 2, "value": 1800 }
      ]
    }]
  }
}
```

- `storage_orders[].packages[].id` — gli `storage_package_ids` da spedire in uscita nel passo 10.
- `warehouses[].available_count` — il numero di colli disponibili per magazzino.

**GraphQL:** `customerShipoutAvailableItems` ([Manuale GraphQL](/api/graphql/documentation#/storage-shipout/customerShipoutAvailableItems))

```graphql
query {
  customerShipoutAvailableItems(warehouse_id: 7) {
    result
    data {
      warehouses { id name available_count }
      storage_orders { id warehouse_id packages { id description weight length width height } }
    }
  }
}
```

**Verifica:** avete annotato uno o più `storage_package_ids` (esempio `5001`). Un elenco vuoto significa che nulla è ancora stato ricevuto — non create un’uscita. `403` significa che l’uscita è disabilitata per questo cliente.

## 10. Stimare e creare l’uscita

Il prezzo di un’uscita è calcolato da un servizio di spedizione dell’azienda. La vostra integrazione elenca i servizi disponibili dal magazzino, stima il prezzo per la destinazione dell’acquirente, quindi crea l’uscita per i colli selezionati.

**REST:** `GET /api/v1/customer/shipout-orders/services?warehouse_id=7` — [Manuale REST](/api/documentation#/paths/v1-customer-shipout-orders-services/get)

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/services?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "services": [
      { "id": 5, "service_code": "intl_express", "name": { "en": "Express" }, "pricing_method": 1, "pricing_method_name": "Shipping Price Plan", "support_multi_package": true }
    ]
  }
}
```

Annotate un `service_code`.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/estimate` — [Manuale REST](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--estimate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/estimate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "delivery_postcode": "K2P1L4",
    "delivery_country": "CA",
    "packages": [{
      "weight": 14,
      "length": 60,
      "width": 40,
      "height": 40,
      "weight_unit": 2,
      "dimension_unit": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "shipping_fee": 38.5,
    "fuel_surcharge": 4.2,
    "handling_fee": 0,
    "sub_total": 42.7,
    "tax": 5.55,
    "total": 48.25,
    "currency": "CAD",
    "has_items_needing_quote": false,
    "refused": false
  }
}
```

- `total` / `currency` — il prezzo stimato per questa destinazione.
- `has_items_needing_quote` — `true` quando il servizio è tariffato manualmente; il magazzino fissa il prezzo dopo la creazione dell’uscita e il pagamento attende tale prezzo.
- `refused` / `refusal_message` — il servizio non accetta questa spedizione perché non è in grado di tariffarla.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/orders` — [Manuale REST](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--orders/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: northwind-order-NW-20931" \
  -d '{
    "warehouse_id": 7,
    "storage_package_ids": [5001],
    "delivery_name": "Maya Chen",
    "delivery_telephone": "6135550142",
    "delivery_email": "maya.chen@example.com",
    "delivery_address_1": "150 Elgin St",
    "delivery_city": "Ottawa",
    "delivery_province": "ON",
    "delivery_country": "CA",
    "delivery_postcode": "K2P1L4",
    "note": "Web order NW-20931"
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 8001,
    "status": 0,
    "is_storage_shipout": true,
    "total_price": "48.25",
    "price_breakdown": { "total": 48.25, "has_items_needing_quote": false },
    "has_items_needing_quote": false,
    "storage_package_ids": [5001]
  }
}
```

- `data.id` — l’id dell’uscita. Conservatelo insieme all’ordine dell’acquirente.
- `data.status` — `0` = in attesa (pagamento non ancora effettuato), `1` = confermata, `2` = in transito, `3` = spedita, `4` = annullata, `5` = non riuscita.
- `storage_package_ids` — questi colli sono ora bloccati su questa uscita e non compaiono più nel passo 9.
- Campi obbligatori: `warehouse_id`, `storage_package_ids`, `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`. Tutti i colli devono provenire dallo stesso magazzino.

**GraphQL:** `customerCreateShipoutOrder` ([Manuale GraphQL](/api/graphql/documentation#/customer/customerCreateShipoutOrder))

```graphql
mutation {
  customerCreateShipoutOrder(
    service_code: "intl_express"
    warehouse_id: 7
    storage_package_ids: [5001]
    delivery_name: "Maya Chen"
    delivery_telephone: "6135550142"
    delivery_email: "maya.chen@example.com"
    delivery_address_1: "150 Elgin St"
    delivery_city: "Ottawa"
    delivery_province: "ON"
    delivery_country: "CA"
    delivery_postcode: "K2P1L4"
    note: "Web order NW-20931"
  ) {
    result
    message
    data { id status total_price has_items_needing_quote storage_package_ids }
  }
}
```

**Verifica:** la risposta contiene un `id` di uscita. I colli di stoccaggio selezionati sono bloccati su questa richiesta.

## 11. Pagare l’uscita

Il magazzino elabora un’uscita una volta pagata. La vostra integrazione paga l’importo residuo dal saldo del cliente; omettete `amount` per pagarlo per intero.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/pay` — [Manuale REST](/api/documentation#/paths/v1-customer-shipout-orders-id--pay/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/8001/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "result": true,
  "data": {
    "transaction_id": 9001,
    "amount": "48.25",
    "order_status": 1,
    "paid_amount": 48.25,
    "remaining_balance": 0
  }
}
```

- `amount` (richiesta, facoltativo) — un importo parziale; per impostazione predefinita è l’intero importo residuo.
- `order_status` — `1` (confermata) dopo il pagamento completo.
- `remaining_balance` — `0` quando il pagamento è completo.

**GraphQL:** `customerPayShipout` ([Manuale GraphQL](/api/graphql/documentation#/storage-shipout/customerPayShipout))

```graphql
mutation {
  customerPayShipout(id: 8001) {
    result
    message
    data { transaction_id amount order_status paid_amount remaining_balance }
  }
}
```

**Verifica:** il pagamento registra un importo (oppure `402` / `422` con un motivo chiaro). `402` significa che il saldo è insufficiente; `422` significa che l’ordine non è ancora pagabile (ad esempio, è ancora in attesa di un preventivo manuale) o che l’importo non è valido.

## 12. Consultare e tracciare l’uscita

La vostra integrazione consulta l’uscita per seguirne lo stato e, una volta che il magazzino l’ha spedita, segue la spedizione tramite il suo numero di tracking.

**REST:** `GET /api/v1/customer/shipout-orders/{id}` — [Manuale REST](/api/documentation#/paths/v1-customer-shipout-orders-id/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipout-orders/8001 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "id": 8001,
    "status": 3,
    "status_label": "Shipped",
    "warehouse": { "id": 7, "name": "Toronto Hub" },
    "shipping_service": { "id": 5, "service_code": "intl_express" },
    "total_price": "48.25",
    "paid_amount": 48.25,
    "remaining_balance": 0,
    "can_be_paid": false,
    "can_be_cancelled": false,
    "storage_packages": [{ "id": 5001, "storage_order_id": 1024, "description": "Insulated jackets, carton of 12" }]
  }
}
```

- `status` / `status_label` — lo stato attuale dell’uscita.
- `can_be_paid` / `can_be_cancelled` — indicano se il passo 11 o il passo 14 sono attualmente consentiti.

Quando esiste un numero di tracking:

**REST:** `GET /api/v1/tracking/{trackingNumber}` — [Manuale REST](/api/documentation#/paths/v1-tracking-trackingNumber/get)

```bash
curl https://YOUR_HOST/api/v1/tracking/SR123456789012
```

```json
{
  "result": true,
  "deliveried": false,
  "data": [
    { "tracking_event_status_id": 3, "description": "Package picked up", "location_city": "Toronto", "updated_at": "2026-12-02 14:30:00" }
  ]
}
```

- `data[]` — gli eventi di tracking in ordine cronologico.
- `deliveried` — `true` quando la spedizione è stata consegnata.

**GraphQL:** `trackingPublic` ([Manuale GraphQL](/api/graphql/documentation#/tracking/trackingPublic))

```graphql
query {
  trackingPublic(trackingNumber: "SR123456789012") {
    result
    deliveried
    data { tracking_event_status_id description location_city updated_at }
  }
}
```

**Verifica:** la consultazione dell’uscita restituisce lo `status` atteso. Il tracking pubblico trova la spedizione una volta che esiste un numero.

## 13. Sottoscrivere i webhook

I webhook inviano al vostro server le variazioni di tracking e di stato, senza necessità di interrogazioni periodiche. Un account cliente imposta i propri URL di webhook e il proprio segreto di firma; le impostazioni sono memorizzate sull’account cliente, non sull’azienda.

**REST:** `PUT /api/v1/webhook-settings` — [Manuale REST](/api/documentation#/paths/v1-webhook-settings/put)

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tracking_event_webhook_url": "https://hooks.northwind-outdoor.example/tracking",
    "order_status_change_webhook_url": "https://hooks.northwind-outdoor.example/status",
    "webhook_sign_secret": "nw-webhook-secret-2026-0123456789"
  }'
```

```json
{
  "result": true,
  "changed_keys": ["tracking_event_webhook_url", "order_status_change_webhook_url", "webhook_sign_secret"],
  "recipient_type": "customer",
  "settings": {
    "webhook_sign_secret": "************6789",
    "tracking_event_webhook_url": "https://hooks.northwind-outdoor.example/tracking",
    "order_status_change_webhook_url": "https://hooks.northwind-outdoor.example/status"
  }
}
```

- Cambiano solo le chiavi inviate; una chiave sconosciuta o un URL non valido restituisce `400`.
- `recipient_type` — `customer` conferma che le impostazioni appartengono all’account cliente.
- `webhook_sign_secret` — da 16 a 255 caratteri; conservatelo sul vostro server per verificare le firme.

**GraphQL:** `webhookSettingsUpdate` ([Manuale GraphQL](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

Verificate la **v2**: `HMAC_SHA256(timestamp + "." + raw_body, secret)` rispetto a `X-Webhook-Signature-V2`. Eliminate i duplicati in base a `X-Webhook-Event-Id`. Rispondete con **2xx entro 3 secondi**.

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

**Verifica:** l’aggiornamento restituisce `changed_keys` con le chiavi inviate, e un evento di prova ricevuto al vostro URL supera il controllo della firma descritto sopra.

## 14. Annullare un’uscita o un ordine di stoccaggio

L’annullamento libera quanto era stato riservato. L’annullamento di un’uscita riporta i suoi colli in giacenza; l’annullamento di un ordine di stoccaggio interrompe una prenotazione la cui merce non è ancora stata ricevuta.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/cancel` — [Manuale REST](/api/documentation#/paths/v1-customer-shipout-orders-id--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/8001/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Buyer cancelled web order NW-20931"}'
```

```json
{ "result": true, "message": "Shipout cancelled." }
```

- `reason` (facoltativo) — registrato insieme all’annullamento.
- Un’uscita può essere annullata solo finché è in attesa (`0`) o confermata (`1`).

**GraphQL:** `customerCancelShipout` ([Manuale GraphQL](/api/graphql/documentation#/storage-shipout/customerCancelShipout))

```graphql
mutation {
  customerCancelShipout(id: 8001, reason: "Buyer cancelled web order NW-20931") {
    result
    message
  }
}
```

Questo rilascia il blocco dei colli di stoccaggio. Lo stoccaggio stesso si annulla con `POST /api/v1/customer/storage-orders/{id}/cancel` finché è ancora consentito (stato `pending payment`, `confirmed`, `waiting for pickup` o `awaiting dropoff`; [Manuale REST](/api/documentation#/paths/v1-customer-storage-orders-id--cancel/post)).

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/1024/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{ "result": true, "message": "Storage order cancelled", "data": { "id": 1024, "status": "cancelled" } }
```

- L’importo già pagato per l’ordine di stoccaggio viene riaccreditato sul saldo del cliente.
- Un ordine di stoccaggio in qualsiasi altro stato restituisce `403`.

**Verifica:** `422` significa che questo stato non può essere annullato. Dopo un annullamento riuscito dell’uscita, il passo 9 elenca di nuovo i colli.

## 15. Gestione degli errori

| Situazione | Stato HTTP | Codice | Cosa fa l’integrazione |
|---|---|---|---|
| Token mancante o scaduto, oppure tipo di account errato | 401 | — | Effettua di nuovo l’accesso come cliente (passo 4). |
| Preventivo di stoccaggio senza `warehouse_id` o date | 400 | — | Invia `warehouse_id`, `start_date` e `end_date`. |
| Convalida dell’ordine di stoccaggio non riuscita (dimensioni dell’articolo mancanti, `end_date` non successiva a `start_date`, `packaging_id` mancante) | 422 | — | Legge `errors`, corregge i campi e invia di nuovo. |
| Pagamento dello stoccaggio con saldo insufficiente, oppure ordine già interamente pagato | 400 | — | Ricarica il saldo (la risposta contiene `customer_balance`), oppure si ferma se l’ordine è già pagato. |
| Pagamento dello stoccaggio con `custom_amount` fuori dall’intervallo consentito | 422 | — | Paga un importo compreso tra il minimo e l’importo residuo. |
| L’ordine di stoccaggio non può essere annullato nel suo stato attuale | 403 | — | Chiede al magazzino di gestire l’ordine; non riprova. |
| Uscita disabilitata per questo cliente | 403 | — | Chiede all’azienda di abilitare l’uscita per l’account cliente. |
| Codice di servizio sconosciuto, oppure uscita / ordine di stoccaggio non trovati | 404 | — | Rilegge l’elenco dei servizi o controlla l’id memorizzato. |
| Collo non disponibile, colli provenienti da magazzini diversi, oppure servizio non offerto dal magazzino | 422 | — | Rilegge il passo 9 e seleziona colli disponibili di un unico magazzino. |
| Il servizio di spedizione non è in grado di tariffare la spedizione e la rifiuta | 422 | `unpriced_refused` | Sceglie un altro servizio o un’altra destinazione; non è stato creato nulla. |
| Pagamento dell’uscita con saldo insufficiente | 402 | — | Ricarica il saldo, quindi ripete il passo 11. |
| Uscita non ancora pagabile (in attesa di un preventivo manuale) o importo non valido | 422 | — | Attende il prezzo, consulta di nuovo l’uscita, quindi paga. |
| L’uscita non può essere annullata nel suo stato attuale | 422 | — | La spedizione è già in corso; non riprova. |
| Stessa `Idempotency-Key` inviata con un corpo diverso | 409 | `IDEMPOTENCY_CONFLICT` | Usa una nuova chiave per una richiesta diversa. |
| Richiesta originale con la stessa `Idempotency-Key` ancora in elaborazione | 409 | `IDEMPOTENCY_IN_PROGRESS` | Attende i secondi indicati in `Retry-After` e invia di nuovo la stessa richiesta. |

## Elenco di verifica

- [ ] La configurazione dello stoccaggio restituisce un `id` di magazzino.
- [ ] Il preventivo di stoccaggio restituisce un prezzo e la creazione dello stoccaggio restituisce `data.id`.
- [ ] Il pagamento dello stoccaggio va a buon fine, **oppure** avete confermato che il portafoglio va ricaricato.
- [ ] Gli articoli disponibili elencano i colli ricevuti (`storage_package_ids`).
- [ ] La stima dell’uscita restituisce un prezzo o `has_items_needing_quote`, e la creazione dell’uscita restituisce un `id` e blocca quei colli.
- [ ] Il pagamento dell’uscita va a buon fine (oppure `402` / `422` è compreso).
- [ ] Il tracking pubblico trova la spedizione una volta che esiste un numero di tracking.
- [ ] L’annullamento dell’uscita rilascia i colli, **oppure** questo stato non può essere annullato.
- [ ] La ripetizione di una creazione con la stessa `Idempotency-Key` e lo stesso corpo restituisce `replayed: true` e nessun secondo ordine.
- [ ] Le impostazioni dei webhook restituiscono `recipient_type: customer`, e un evento ricevuto supera il controllo della firma v2.
