# Servizi di spedizione

L’API dei servizi di spedizione consente a un account cliente di prenotare i servizi di spedizione che il suo fornitore logistico ha configurato e gli ha assegnato. Il sistema del cliente elenca i servizi che può utilizzare, carica le regole di un servizio, quota una spedizione, crea l’ordine di spedizione, lo paga con il saldo dell’account e segue la spedizione fino alla consegna. Questa guida è rivolta agli sviluppatori che collegano il sistema di un importatore, di un commerciante o di un grossista al fornitore logistico che lo serve.

## 1. Cosa potete realizzare

Tutti gli esempi di questa guida usano un unico scenario. **Harbourline Imports Inc.**, un importatore di tè di Toronto, dispone di un account cliente presso il proprio fornitore logistico. Il fornitore offre il servizio `intl_express` (International Express) dal suo magazzino Toronto Hub (id magazzino `7`). Harbourline consegna due cartoni di tè campione al Toronto Hub per un distributore di Seattle, con il proprio ordine d’acquisto `HLI-PO-1058`.

- **Prenotazione dal sistema degli ordini d’acquisto.** Quando un ordine d’acquisto viene rilasciato, il sistema di Harbourline quota la spedizione su `intl_express`, crea l’ordine di spedizione con il numero dell’ordine d’acquisto come riferimento e lo paga con il saldo prepagato dell’account, senza che nessuno apra il portale del fornitore.
- **Una verifica del prezzo prima dell’impegno.** L’addetto agli acquisti di Harbourline vede il nolo, i supplementi, le imposte e il totale per i due cartoni prima che la spedizione venga prenotata, e una spedizione che il servizio non può quotare viene fermata prima che esista un ordine.
- **Stato della spedizione all’interno dell’ERP.** Il numero di tracking di ogni cartone viene memorizzato in corrispondenza dell’ordine d’acquisto; i webhook trasferiscono lo stato dell’ordine e la cronologia di tracking nell’ERP, e un job notturno esegue la riconciliazione con l’elenco degli ordini.
- **Modifiche controllate.** Una prenotazione non pagata viene corretta direttamente, e una prenotazione non più necessaria viene annullata con l’importo pagato restituito al credito dell’account.

## 2. Cosa copre questa guida

Usate questa famiglia di endpoint quando il chiamante è un **cliente** dell’azienda di logistica e prenota uno dei servizi di spedizione propri dell’azienda: l’azienda definisce il piano tariffario, i magazzini, i supplementi e gli imballaggi, e assegna i servizi al cliente. Il cliente vede e prenota soltanto i servizi che gli sono assegnati.

Usate un’altra famiglia di endpoint nei casi seguenti:

- Il chiamante è l’azienda di logistica stessa (un account aziendale/cliente) e prenota ritiri e consegne in giornata o locali con la propria flotta: leggete **Ritiro e consegna (flotta propria)**.
- Il chiamante acquista etichette corriere (ad esempio UPS o FedEx) alle tariffe negoziate dell’account: leggete **Etichette corriere**.
- Il cliente deposita merci nel magazzino del fornitore e le spedisce dalle giacenze: leggete **Stoccaggio e uscita**.

**Uniorder: un'API per ogni spedizione** (`/api/v1/uniorder/...`) è il punto di ingresso unico consigliato per le nuove integrazioni di consegna locale e di etichette corriere. Uniorder non copre i servizi di spedizione: gli ordini dei servizi di spedizione vengono creati e gestiti esclusivamente tramite gli endpoint `/api/v1/customer/shipping-orders/...` descritti qui.

## 3. Prima di iniziare

- **Tipo di account.** Un account **cliente** dell’azienda di logistica, con l’**autorizzazione API** abilitata dall’azienda. Un account aziendale/cliente o un account dipendente non può accedere tramite il login cliente descritto sotto.
- **Assegnazione dei servizi.** L’azienda deve assegnare al cliente almeno un servizio di spedizione attivo. Un cliente senza servizi assegnati riceve un elenco di servizi vuoto.
- **Dati di prova.** Concordate con l’azienda un codice di servizio di prova, un magazzino di prova e un piccolo saldo prepagato sull’account di prova. Usate un riferimento come `HLI-PO-1058` o `DEV-SHIP-001`, in modo che gli ordini di prova siano facili da trovare e annullare.
- **Gestione del token.** Chiamate l’API solo dal vostro server. Tenete la password e il token di accesso fuori dai browser e dai client mobili. Il token scade una settimana dopo il login (`expires_at`); eseguite di nuovo il login prima della scadenza.
- **Segnaposto.** Sostituite `YOUR_HOST` con il nome host dell’azienda di logistica e `ACCESS_TOKEN` con il token restituito dal passo di login.
- **Errori in JSON.** Inviate `Accept: application/json` in ogni richiesta, in modo che gli errori di validazione restituiscano JSON anziché un reindirizzamento.

## 4. Accesso come cliente

Il login scambia l’e-mail e la password del cliente con un token bearer. Ogni chiamata successiva di questa guida invia quel token.

**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" \
  -H "Accept: application/json" \
  -d '{"email":"logistics@harbourline-imports.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2026-10-05 09:15:00",
  "expires_timestamp": 1791206100,
  "name": "Harbourline Imports Inc."
}
```

- `access_token`: inviatelo in ogni richiesta come `Authorization: Bearer ACCESS_TOKEN`. GraphQL usa la stessa intestazione su `POST /api/graphql`.
- `expires_at` / `expires_timestamp`: pianificate un nuovo login prima di questo momento.

**Verifica:** la risposta contiene `result: true` e un `access_token`. Una richiesta senza il token restituisce `401`; anche un login con un account che non è un cliente, o che non dispone dell’autorizzazione API, restituisce `401`.

## 5. Elencare i servizi assegnati al cliente

L’elenco dei servizi indica all’integrazione quali codici di servizio può prenotare e se ciascun servizio accetta la consegna in magazzino, il ritiro o entrambi. Memorizzate il `service_code`; ogni chiamata successiva relativa al servizio lo utilizza.

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "services": [
      {
        "id": 12,
        "service_code": "intl_express",
        "name": { "en": "International Express" },
        "offer_pickup": true,
        "allow_warehouse_delivery": true,
        "support_multi_package": true,
        "allow_special_requirements": false,
        "allow_purchase_supplies": true,
        "send_confirmation_email": true,
        "warehouses": [{ "id": 7, "name": "Toronto Hub" }]
      }
    ]
  }
}
```

- `service_code`: il parametro di percorso di ogni chiamata successiva relativa al servizio.
- `offer_pickup` / `allow_warehouse_delivery`: i valori consentiti di `origin_type` (`pickup` / `warehouse`).
- `support_multi_package`: indica se un ordine può contenere più di una riga di colli.
- Un array `services` vuoto significa che a questo cliente non è assegnato alcun servizio.

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

```graphql
query CustomerShippingOrderServices {
  customerShippingOrderServices
}
```

**Verifica:** l’elenco contiene almeno un servizio e avete memorizzato il suo `service_code` (in questa guida: `intl_express`).

## 6. Caricare la configurazione del servizio

La configurazione restituisce tutto ciò di cui ha bisogno il modulo d’ordine di un servizio: i magazzini che accettano le consegne, i supplementi selezionabili, il catalogo di imballaggi e materiali, le unità di misura e i paesi da cui il servizio può ritirare e verso cui può consegnare. Convalidate i dati dell’ordine rispetto a essa prima di quotare o creare qualsiasi cosa.

**REST:** `GET /api/v1/customer/shipping-orders/services/{serviceCode}/config` — [Manuale REST](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--config/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/config \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "service": {
      "service_code": "intl_express",
      "offer_pickup": true,
      "allow_warehouse_delivery": true,
      "allow_special_requirements": false,
      "allow_purchase_supplies": true,
      "weight_mode": 2,
      "volumetric_factor": 5000
    },
    "warehouses": [
      {
        "id": 7,
        "name": "Toronto Hub",
        "address_1": "250 Dundas St W",
        "city": "Toronto",
        "province": "ON",
        "country": "CA",
        "postcode": "M5T 2Z5",
        "telephone": "4165550100"
      }
    ],
    "surcharges": [],
    "has_surcharges": false,
    "packagings": [],
    "products": [],
    "weight_units": { "2": { "name": "weight_kg", "accuracy": 3 } },
    "dimension_units": { "2": { "name": "dimension_cm", "accuracy": 1 } },
    "delivery_allowed_countries": ["CA", "US"],
    "pickup_allowed_countries": ["CA"]
  }
}
```

- `warehouses[].id`: il `warehouse_id` da inviare quando `origin_type` è `warehouse`. Un id non presente in questo elenco viene rifiutato alla creazione.
- `service.weight_mode`: i campi del collo richiesti dal piano tariffario: `0` peso reale (peso), `1` peso volumetrico (lunghezza, larghezza e altezza), `2` peso tassabile (entrambi). `null` significa che il servizio viene quotato manualmente. Inviate il peso e tutte e tre le dimensioni per soddisfare ogni modalità.
- `delivery_allowed_countries` / `pickup_allowed_countries`: rifiutate un paese di destinazione o di ritiro non presente in questi elenchi prima di chiamare la stima.
- `surcharges[].id`, `packagings[].id`, `products[].id`: gli id da usare per i supplementi facoltativi, gli imballaggi e gli acquisti di materiali.
- `weight_units` / `dimension_units`: le unità dei colli vengono inviate come numeri. Inviate `weight_unit: 2` (kg) e `dimension_unit: 2` (cm), come fanno tutti gli esempi di questa guida; entrambi sono anche i valori predefiniti quando i campi vengono omessi.

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

```graphql
query CustomerShippingOrderServiceConfig {
  customerShippingOrderServiceConfig(serviceCode: "intl_express")
}
```

**Verifica:** `result` è `true` e, per una consegna in magazzino, `warehouses` contiene il magazzino che intendete utilizzare. `403` significa che il servizio non è assegnato a questo cliente; `404` significa che il codice di servizio non esiste o non è attivo.

## 7. Stimare il prezzo

La stima quota la spedizione con il piano tariffario del servizio senza scrivere nulla. Mostrate il totale all’acquirente e non create l’ordine quando la stima segnala un rifiuto.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/estimate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_postcode": "98104",
    "delivery_country": "US",
    "packages": [{
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "needs_manual_quote": false,
    "shipping_fee": 118.40,
    "shipping_fee_total": 131.20,
    "fuel_surcharge": 12.80,
    "pickup_fee": null,
    "surcharges_total": 0,
    "sub_total": 131.20,
    "tax": { "total_tax": 17.06 },
    "total": 148.26,
    "currency": "CAD",
    "all_fees_calculated": true,
    "has_items_needing_quote": false,
    "unpriced_items": [],
    "refused": false,
    "refusal_message": null
  }
}
```

- `origin_type`: `warehouse` (il cliente consegna la merce in un magazzino; inviate `warehouse_id`) oppure `pickup` (il fornitore ritira; inviate `pickup_postcode` e `pickup_country`). Usate solo un valore consentito dal passo 5.
- `packages`: una riga per ogni gruppo di colli identici; `quantity` moltiplica la riga.
- `total` e `currency`: l’importo da mostrare. `total` è `null` finché una qualsiasi tariffa non è calcolata.
- `needs_manual_quote` / `has_items_needing_quote`: l’azienda quota l’ordine manualmente; l’ordine può essere creato e viene pagato dopo che l’azienda ha fissato il prezzo.
- `refused` / `refusal_message`: il servizio rifiuta le spedizioni che non può quotare. Non create l’ordine; mostrate invece `refusal_message`.
- Input facoltativi: `surcharges`, `products` (una mappa da id prodotto a quantità, considerata solo quando `allow_purchase_supplies` è true), `has_special_requirements`, `coupon_code`.

**Verifica:** `result` è `true`, `refused` è `false` e `total` ha un valore oppure `needs_manual_quote` è `true`.

## 8. Creare l’ordine di spedizione

La chiamata di creazione prenota la spedizione sul servizio. L’integrazione memorizza l’`id` restituito in corrispondenza del proprio ordine d’acquisto; ogni chiamata successiva utilizza questo id.

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

Inviate un `Idempotency-Key` derivato da un vostro id stabile (qui il numero dell’ordine d’acquisto). Un nuovo tentativo con la stessa chiave e lo stesso corpo restituisce la prima risposta con `"replayed": true` e l’intestazione `Idempotency-Replayed: true`, e non crea un secondo ordine.

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: hli-po-1058" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "HLI-PO-1058",
    "delivery_name": "Cascade Tea Distributors",
    "delivery_telephone": "2065550143",
    "delivery_email": "receiving@cascadetea.example",
    "delivery_address_1": "300 5th Ave S",
    "delivery_city": "Seattle",
    "delivery_province": "WA",
    "delivery_country": "US",
    "delivery_postcode": "98104",
    "note": "Two cartons of sample tea, dock door B",
    "package": [{
      "description": "Sample tea carton",
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "value": 380,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 0,
    "total_price": 148.26,
    "price_breakdown": { "total": 148.26, "currency": "CAD" },
    "promotion_id": null,
    "tracking_number": null
  }
}
```

- La chiave del corpo per i colli è `package` nella creazione (è `packages` nella stima). Ogni riga con `quantity` N diventa N colli, e ogni collo riceve il proprio numero di tracking.
- Campi obbligatori: `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`, `origin_type`, `package[].weight`; più `warehouse_id` per `warehouse`, oppure `pickup_name`, `pickup_telephone`, `pickup_address_1`, `pickup_city`, `pickup_province`, `pickup_country`, `pickup_postcode` per `pickup`.
- Campi facoltativi: `reference` (memorizzato come `reference_number` dell’ordine), `delivery_email`, `delivery_address_2`, `scheduled_date`, `time_window`, `note`, `special_requirements` (un array di righe di testo, considerato solo quando il servizio lo consente), `products`, `surcharges`, `coupon_code`.
- `id`: memorizzatelo. `status` `0` è In attesa (in attesa di pagamento).
- `total_price`: l’importo addebitato dal passo 9. È `0` finché l’ordine attende un preventivo manuale.
- `tracking_number` a livello di ordine è `null`; i numeri di tracking si trovano sui colli e vengono letti al passo 10.
- L’endpoint risponde con HTTP `201` per un nuovo ordine.

**Verifica:** la risposta contiene `result: true` e un `id`. Ripetendo la stessa richiesta con lo stesso `Idempotency-Key` si ottiene lo stesso `id` con `"replayed": true`.

## 9. Pagare l’ordine con il saldo dell’account

Gli ordini di spedizione vengono pagati per intero con il saldo dell’account del cliente. Un ordine pagato passa da In attesa a Confermato, e il fornitore inizia a gestirlo.

Leggete prima l’importo:

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001/payment-info \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "order_id": 9001,
    "currency": "CAD",
    "total_price": "148.26",
    "paid_amount": "0.00",
    "remaining_balance": "148.26",
    "user_balance": "500.00",
    "has_sufficient_balance": true,
    "shortfall": 0,
    "payment_options": [
      { "type": "remaining_balance", "amount": 148.26 }
    ]
  }
}
```

Quindi pagate:

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

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

```json
{
  "result": true,
  "message": "Payment of $148.26 processed successfully.",
  "data": {
    "order_id": 9001,
    "status": 1,
    "amount_paid": "148.26"
  }
}
```

- `has_sufficient_balance` / `shortfall`: quando il saldo non copre `remaining_balance`, ricaricate l’account prima di pagare.
- `payment_type`: è supportato solo `remaining_balance`; viene sempre addebitato l’intero importo residuo.
- `data.status` `1` è Confermato.

**Verifica:** la chiamata di pagamento restituisce `result: true` e `status` `1`, e una seconda chiamata a `payment-info` restituisce `400` perché l’ordine è interamente pagato. Una chiamata di pagamento senza saldo sufficiente restituisce `422` e non addebita nulla.

## 10. Consultare l’ordine e tracciare i colli

La chiamata di dettaglio restituisce lo stato attuale e il numero di tracking di ogni collo. Memorizzate i numeri di tracking dei colli in corrispondenza dell’ordine d’acquisto; il tracking pubblico accetta ciascuno di essi.

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 1,
    "status_name": "Confirmed",
    "can_edit": false,
    "can_cancel": true,
    "shipping_code": "K7RW2Q",
    "tracking_number": null,
    "reference_number": "HLI-PO-1058",
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_address": { "name": "Cascade Tea Distributors", "city": "Seattle", "country": "US" },
    "package_count": 2,
    "packages": [
      { "id": 55101, "description": "Sample tea carton", "tracking_number": "SR123456789012", "weight": 12, "weight_unit": 2 },
      { "id": 55102, "description": "Sample tea carton", "tracking_number": "SR123456789013", "weight": 12, "weight_unit": 2 }
    ],
    "total_price": 148.26
  }
}
```

- `status`: `0` In attesa, `1` Confermato, `2` In transito, `3` Spedito, `4` Annullato, `5` Non riuscito, `6` Ritirato parzialmente, `7` Ritirato, `8` In lavorazione.
- `can_edit` / `can_cancel`: indicano se il passo 12 è attualmente consentito.
- `packages[].tracking_number`: i numeri da memorizzare e da tracciare.
- `shipping_code`: il codice accettato dalle schermate di consegna in magazzino; stampatelo sui documenti di consegna.

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

```graphql
query CustomerShippingOrderShow {
  customerShippingOrderShow(id: 9001) {
    result
    message
    data {
      id
      status
      status_name
      can_cancel
      reference_number
      packages {
        id
        tracking_number
        weight
      }
      total_price
    }
  }
}
```

Per riconciliare tutti gli ordini di un servizio, ad esempio in un job notturno, elencateli con un filtro. Il filtro `id` corrisponde all’id dell’ordine, a un numero di tracking o al riferimento.

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

```bash
curl "https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders?id=HLI-PO-1058&created_at_from=2026-09-01&per_page=20" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

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

Il tracking pubblico non richiede token e restituisce la cronologia degli eventi di un collo:

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

```bash
curl https://YOUR_HOST/api/v1/tracking/SR123456789012 \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 1,
      "otep_status": "received",
      "description": "Received at warehouse",
      "location_city": "Toronto",
      "updated_at": "2026-09-29 10:42:00"
    }
  ]
}
```

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

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

**Verifica:** il dettaglio restituisce l’ordine di questo cliente con un numero di tracking per ogni collo, e il tracking pubblico restituisce `result: true` per il numero di tracking di un collo. L’id di un ordine di un altro cliente restituisce `404`.

## 11. Ricevere i webhook

I webhook recapitano al vostro server la creazione degli ordini, i cambi di stato e gli eventi di tracking, così l’integrazione non deve interrogare periodicamente l’API. L’account cliente configura i propri URL dei webhook e il proprio segreto di firma.

Quando viene creato un ordine di spedizione, il fornitore crea anche un ordine di ritiro collegato per il proprio team di dispatch. I webhook vengono inviati per quell’ordine collegato: il suo `ref` è `Shipping-Pickup-{shipping order id}` (ad esempio `Shipping-Pickup-9001`), e ciascuno dei suoi colli riporta il numero di tracking del collo di spedizione in `external_tracking_number`. Associate gli eventi in arrivo in base a questi due campi.

| Impostazione | Evento | Cosa fa l’integrazione |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Collega l’evento all’ordine di spedizione tramite `ref` e `packages[].external_tracking_number` |
| `tracking_event_webhook_url` | `tracking.event` | Aggiunge l’evento alla cronologia del collo |
| `order_status_change_webhook_url` | `order.status_change` | Aggiorna lo stato mostrato nel vostro sistema |

**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" \
  -H "Accept: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.harbourline-imports.example/hooks/order-created",
    "tracking_event_webhook_url": "https://erp.harbourline-imports.example/hooks/tracking",
    "order_status_change_webhook_url": "https://erp.harbourline-imports.example/hooks/status",
    "webhook_sign_secret": "hli-webhook-secret-7f2c9a1e5b"
  }'
```

```json
{
  "result": true,
  "changed_keys": [
    "order_create_webhook_url",
    "tracking_event_webhook_url",
    "order_status_change_webhook_url",
    "webhook_sign_secret"
  ],
  "recipient_type": "customer",
  "settings": {
    "webhook_sign_secret": "*************************1e5b",
    "tracking_event_webhook_url": "https://erp.harbourline-imports.example/hooks/tracking"
  }
}
```

- Cambiano solo le chiavi inviate; una stringa vuota cancella un URL. `webhook_sign_secret` deve avere da 16 a 255 caratteri, e nessun webhook viene inviato finché il segreto è vuoto.
- `recipient_type` è `customer` per un account cliente.

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

```graphql
mutation WebhookSettingsUpdate {
  webhookSettingsUpdate(
    order_status_change_webhook_url: "https://erp.harbourline-imports.example/hooks/status"
    tracking_event_webhook_url: "https://erp.harbourline-imports.example/hooks/tracking"
  )
}
```

Verificate la firma **v2** sul corpo grezzo: `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** ed elaborate l’evento in seguito.

```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:** dopo la chiamata alle impostazioni, una creazione di prova produce un evento `order.created` il cui `ref` è `Shipping-Pickup-{id}` per l’id del nuovo ordine di spedizione, e la verifica della firma ha esito positivo.

## 12. Modificare o annullare un ordine

Un ordine può essere corretto finché è In attesa (prima del pagamento) e annullato finché è In attesa o Confermato. L’annullamento di un ordine pagato restituisce l’importo pagato al credito dell’account.

Per modificarlo, inviate di nuovo l’ordine completo con gli stessi campi del passo 8. Il prezzo viene ricalcolato.

**REST:** `PUT /api/v1/customer/shipping-orders/{id}` — [Manuale REST](/api/documentation#/paths/v1-customer-shipping-orders-id/put)

```bash
curl -X PUT https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "HLI-PO-1058",
    "delivery_name": "Cascade Tea Distributors",
    "delivery_telephone": "2065550143",
    "delivery_email": "receiving@cascadetea.example",
    "delivery_address_1": "300 5th Ave S",
    "delivery_address_2": "Suite 210",
    "delivery_city": "Seattle",
    "delivery_province": "WA",
    "delivery_country": "US",
    "delivery_postcode": "98104",
    "package": [{
      "description": "Sample tea carton",
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "value": 380,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 0,
    "total_price": 148.26,
    "promotion_id": null,
    "coupon_code": null,
    "promotion_discount": null
  }
}
```

Per annullarlo:

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

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

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 4,
    "refund_amount": 148.26
  },
  "message": "Order cancelled. $148.26 refunded to your credit."
}
```

- `status` `4` è Annullato. L’ordine di ritiro collegato viene rimosso.
- `refund_amount`: l’importo restituito al credito dell’account; `0` per un ordine non pagato.
- Leggete `can_edit` e `can_cancel` dal passo 10 prima di offrire queste azioni agli utenti.

**Verifica:** l’annullamento restituisce `status` `4` e il dettaglio mostra `status_name` `Cancelled`. Un secondo annullamento, o l’annullamento di un ordine In transito o in uno stato successivo, restituisce `403` con il messaggio `This order can no longer be cancelled.`; la modifica di un ordine pagato restituisce `403`.

## 13. Gestione degli errori

| Situazione | Stato HTTP | Codice | Cosa fa l’integrazione |
|---|---|---|---|
| Token mancante, scaduto o non valido; login con un account non cliente o senza autorizzazione API | `401` | — | Esegue di nuovo il login; se il login stesso non riesce, chiede all’azienda di verificare il tipo di account e l’autorizzazione API |
| Il servizio non è assegnato a questo cliente | `403` | — | Rilegge l’elenco dei servizi (passo 5) e prenota solo i servizi assegnati |
| L’API viene chiamata da una sessione di un’app della piattaforma la cui app ha gli ordini di spedizione disabilitati | `403` | `APP_CAPABILITY_DISABLED` | Chiede all’azienda di abilitare gli ordini di spedizione per l’app |
| Codice di servizio sconosciuto o non attivo; id ordine non trovato per questo cliente | `404` | — | Aggiorna l’elenco dei servizi; verifica l’id ordine memorizzato |
| Campo obbligatorio mancante o non valido | `422` | — | Legge `errors` nel corpo, corregge i campi e invia di nuovo |
| Tipo di origine non offerto dal servizio, o magazzino non presente nell’elenco del servizio | `422` | — | Usa un `origin_type` e un `warehouse_id` ricavati dai passi 5 e 6 |
| Il servizio non può quotare la spedizione e rifiuta le spedizioni non quotate | `422` | `unpriced_refused` | Non è stato creato nulla; mostra `message` e non ripete la richiesta invariata |
| Materiali ordinati non disponibili a magazzino | `422` | — | Legge `stock_shortages`, riduce le quantità e invia di nuovo |
| Lo stesso `Idempotency-Key` con un corpo diverso | `409` | `IDEMPOTENCY_CONFLICT` | Usa una nuova chiave per un nuovo ordine; non riutilizza mai una chiave per un contenuto diverso |
| Un nuovo tentativo mentre la prima richiesta con quella chiave è ancora in elaborazione | `409` | `IDEMPOTENCY_IN_PROGRESS` | Attende i secondi indicati in `Retry-After`, quindi riprova con la stessa chiave e lo stesso corpo |
| Pagamento senza saldo sufficiente | `422` | — | Ricarica l’account, quindi paga di nuovo |
| Informazioni di pagamento o pagamento su un ordine interamente pagato | `400` | — | Considera l’ordine come pagato; legge il dettaglio |
| Annullamento dopo che l’ordine ha lasciato lo stato In attesa o Confermato | `403` | — | Mostra che l’ordine non può più essere annullato; contatta l’azienda |
| Modifica dopo il pagamento | `403` | — | Annulla e crea un nuovo ordine, oppure contatta l’azienda |
| Errore del server durante stima, creazione, pagamento o annullamento | `500` | — | Riprova una volta; per la creazione, riprova con lo stesso `Idempotency-Key` |

## Elenco di verifica

Usate un riferimento di prova come `DEV-SHIP-001` o `HLI-PO-1058`:

- [ ] Il login cliente restituisce `access_token`; una richiesta senza il token restituisce `401`.
- [ ] L’elenco dei servizi non è vuoto e avete memorizzato un `service_code`.
- [ ] La configurazione restituisce i magazzini, le unità e i paesi consentiti per quel servizio, e il vostro modulo li utilizza.
- [ ] La stima restituisce un `total` (oppure `needs_manual_quote: true`), e una spedizione rifiutata non viene creata.
- [ ] La creazione restituisce un `id`; lo stesso `Idempotency-Key` con lo stesso corpo restituisce lo stesso `id` con `"replayed": true`.
- [ ] Il pagamento riesce e lo stato diventa Confermato, oppure avete verificato che un saldo insufficiente restituisce `422` e non addebita nulla.
- [ ] Il dettaglio mostra l’ordine di questo cliente con un numero di tracking per ogni collo, e il tracking pubblico trova ogni collo.
- [ ] I webhook sono configurati con un segreto di firma; una creazione di prova produce `order.created` con `ref` `Shipping-Pickup-{id}` e la verifica della firma ha esito positivo.
- [ ] L’annullamento dell’ordine di prova restituisce `status` `4` e il `refund_amount` previsto; un secondo annullamento restituisce `403`.
