# Etichette corriere

Il servizio etichette acquista etichette di spedizione dai corrieri collegati a un account (ad esempio UPS e Canada Post) e conserva ogni etichetta come ordine. Un’integrazione elenca i metodi di spedizione dell’account, quota un collo, crea l’ordine etichetta, acquista l’etichetta al servizio del corriere scelto, stampa il PDF, traccia il collo e annulla le etichette non utilizzate. È destinato a negozi online, sistemi di magazzino e sistemi di gestione ordini che spediscono colli tramite corrieri anziché tramite i propri autisti.

## 1. Cosa potete realizzare

Gli esempi di questa guida seguono un’unica azienda: **Northbound Outfitters**, un negozio online di attrezzatura outdoor che spedisce dal proprio magazzino in 1200 Eglinton Ave E, Toronto. Il suo account dispone di un metodo Canada Post e di un metodo UPS. Un ordine tipico è la scatola di una tenda da 4,2 kg, 60 × 30 × 25 cm, destinata a Calgary; gli ordini verso gli Stati Uniti viaggiano con UPS.

- **Scelta del corriere al checkout.** Il negozio quota il carrello del cliente con Canada Post, mostra i servizi con prezzo e giorni di transito e spedisce con il servizio pagato dal cliente.
- **Stampa delle etichette in magazzino con un clic.** La postazione di imballaggio crea l’ordine etichetta quando una scatola è imballata, acquista l’etichetta al servizio scelto e stampa il PDF del corriere su una stampante termica.
- **Spedizioni transfrontaliere con dati doganali.** Gli ordini verso gli Stati Uniti contengono righe articolo (descrizione, quantità, valore, codice HS), in modo che l’etichetta UPS sia emessa con i relativi dati commerciali.
- **Aggiornamenti di stato automatici al cliente.** Il negozio memorizza il numero di tracking del corriere, mostra la cronologia di tracking pubblica nella pagina dell’ordine e aggiorna l’ordine quando un webhook `tracking.event` segnala che il collo è stato consegnato.

## 2. Cosa copre questa guida

Questa guida copre il servizio etichette v1 (`/api/v1/labelservice/...`): un metodo di spedizione (un account corriere) per chiamata. Usatelo quando l’integrazione sa già con quale metodo di spedizione spedisce, oppure quando mantiene un’integrazione esistente con il servizio etichette.

Per le nuove integrazioni, Uniorder (`/api/v1/uniorder/...`) è il punto di accesso unico raccomandato. Le guide Uniorder, «Uniorder: un'API per ogni spedizione» e «Preventivo e ordine in un unico flusso», quotano contemporaneamente tutti i servizi corriere dell’account (insieme alla consegna propria dell’azienda, ove applicabile) e acquistano l’etichetta al servizio scelto restituendone il `rate_id`. Le stesse chiamate servono poi a stampare, tracciare e annullare ogni ordine.

Altre guide coprono le altre famiglie di spedizione:

- Consegna tramite gli autisti propri dell’azienda: «Ritiro e consegna (flotta propria)».
- Un account cliente che spedisce tramite i servizi offerti dalla propria azienda: «Servizi di spedizione».
- Merce custodita in un magazzino e spedita su richiesta: «Stoccaggio e uscita».

## 3. Prima di iniziare

- **Account.** Usate un account aziendale (client), un dipendente di tale account oppure un account cliente di un’azienda. Un account aziendale vede i propri metodi di spedizione. Un account cliente vede solo i metodi che la propria azienda gli ha assegnato, e ogni etichetta che acquista è addebitata sul suo saldo; quando l’azienda ha attivato Auto Pause Label Service per quel cliente, un’etichetta viene rifiutata finché saldo più credito non la coprono.
- **Autorizzazione API.** L’account deve avere l’accesso API abilitato. In caso contrario ogni chiamata al servizio etichette restituisce `401` con `Unauthorized`.
- **Metodi di spedizione.** Almeno un metodo di spedizione deve essere attivo sull’account (per i clienti: assegnato al cliente). Gli identificatori dei metodi differiscono per account e non devono essere codificati nel codice; leggeteli al passo 5.
- **Dati di prova.** Usate un metodo di spedizione di prova o una sandbox del corriere, ove configurata (le tariffe riportano allora `test_mode: true`), e una destinazione che controllate. Annullate ogni etichetta di prova acquistata su un metodo reale.
- **Gestione del token.** Eseguite l’accesso dal vostro server e conservate il token lì. Non inserite il token né la password in un’applicazione browser o mobile.
- **Segnaposto.** Sostituite `YOUR_HOST` con l’host della vostra piattaforma e `ACCESS_TOKEN` con il token del passo 4. Sostituite i valori di `shipping_method` con gli id del vostro account.

## 4. Autenticazione

Ogni chiamata al servizio etichette richiede un bearer token. L’integrazione esegue l’accesso una volta, memorizza `access_token` e `expires_at` sul server ed esegue di nuovo l’accesso prima che il token scada.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"email":"shipping@northbound-outfitters.ca","password":"your_password"}'
```

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

- `access_token`: inviatelo in ogni chiamata successiva come l’intestazione seguente.
- `expires_at`: eseguite di nuovo l’accesso prima di questo momento.

```
Authorization: Bearer ACCESS_TOKEN
```

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

**GraphQL:** `userLogin` ([Manuale GraphQL](/api/graphql/documentation#/user/userLogin))

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

## 5. Elencare i metodi di spedizione

L’elenco dei metodi indica all’integrazione con quali account corriere può spedire e quali opzioni accetta ciascuno. Memorizzate l’`id` di ogni metodo che usate; è lo `shipping_method` di ogni chiamata successiva.

**REST:** `POST /api/v1/labelservice/getShippingMethodList` — [Manuale REST](/api/documentation#/paths/v1-labelservice-getShippingMethodList/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingMethodList \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"detail": true}'
```

```json
[
  {
    "id": 59,
    "name": "Canada Post",
    "unique_identifier": "CPC-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    },
    "package_type": {
      "parcel": {
        "name": "Parcel",
        "options": { "weight_options": true, "dimension_options": true }
      }
    },
    "from_contry_limit": ["CA"],
    "isUploadMethod": false
  },
  {
    "id": 61,
    "name": "UPS",
    "unique_identifier": "UPS-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    }
  }
]
```

Ogni riga ha:

| Campo | Uso |
|---|---|
| `id` | `shipping_method` in ogni chiamata successiva |
| `name` | Nome visualizzato |
| `unique_identifier` | Codice stabile |
| `options.signature_option` | Firma disponibile |
| `options.insurance_option` | Assicurazione disponibile |
| `options.multi_package` | Più di un pezzo |
| `package_type` | Codici `package_type` accettati e se ciascuno richiede peso e dimensioni |
| `from_contry_limit` | Paesi in cui può trovarsi l’indirizzo del mittente |
| `services` | Corrieri e servizi dietro il metodo; i codici possono limitare una quotazione con `carriers` / `services` |

Inviate `"id": 59` per leggere un solo metodo, oppure `"detail": false` per ricevere solo `id`, `name` e `unique_identifier`.

**GraphQL:** `labelserviceGetShippingMethodList` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceGetShippingMethodList)) (scalare JSON).

**Verifica:** l’elenco non è vuoto. Avete scelto un `id` e sapete se quel metodo consente firma, assicurazione e più colli. Un elenco vuoto significa che nessun metodo è abilitato sull’account.

## 6. Quotare

Una quotazione chiede i prezzi al corriere senza creare nulla: l’ordine temporaneo usato per la richiesta viene eliminato e non viene addebitato nulla. Northbound Outfitters la chiama al checkout per mostrare i servizi Canada Post per il carrello. Il corpo ha la stessa forma del passo 7. `shipping_method` è obbligatorio.

**REST:** `POST /api/v1/labelservice/rate` — [Manuale REST](/api/documentation#/paths/v1-labelservice-rate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "transit_days": 3,
      "test_mode": false
    },
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "transit_days": 2,
      "test_mode": false
    }
  ],
  "best_rate": {
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86,
    "transit_days": 3
  }
}
```

- `rates[]`: una voce per servizio del corriere, con `price`, `currency`, `transit_days` e `price_detail` (tariffa base, supplemento carburante, imposte). Mostrateli al cliente.
- `best_rate` / `shipping_price`: la prima tariffa restituita dal metodo.
- Una quotazione non contiene né `rate_id` né l’`id` dell’ordine. Le etichette si acquistano dalle tariffe dell’ordine creato al passo 7.

`weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in.

Per più di un pezzo, inviate `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (id rubrica) o `shipping_from_code` può sostituire il blocco `sender_*`. `carriers` e `services` limitano la quotazione ai codici elencati.

**GraphQL:** `labelserviceRate` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceRate)).

**Verifica:** `result` è true e avete un prezzo (e i giorni di transito, se il corriere li invia). Se non c’è tariffa, correggete destinazione / collo / metodo **prima** di creare.

## 7. Creare l’ordine etichetta

Questa chiamata crea l’ordine etichetta e chiede al corriere le tariffe di quella spedizione. Restituisce l’`id` dell’ordine e un `rate_id` per servizio. In questa fase l’etichetta non è ancora acquistata e non viene addebitato nulla; il passo 8 la acquista. Northbound Outfitters la chiama quando la scatola è imballata e memorizza `id` in corrispondenza del proprio ordine `NB-10482`.

**REST:** `POST /api/v1/labelservice/submitOrder` — [Manuale REST](/api/documentation#/paths/v1-labelservice-submitOrder/post)

Stesso corpo del passo 6. Inviate `Idempotency-Key`: un nuovo tentativo con la stessa chiave e lo stesso corpo restituisce la prima risposta invece di creare un secondo ordine.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10482-label" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "id": 128455,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
      "carrier_name": "canadapost",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "currency": "CAD",
      "transit_days": 3
    },
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c11",
      "carrier_name": "canadapost",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "currency": "CAD",
      "transit_days": 2
    }
  ],
  "best_rate": {
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86
  }
}
```

| Campo | Uso |
|---|---|
| `id` | Id ordine Superroute — acquisto, download e annullamento |
| `rates[].rate_id` | Il servizio da acquistare al passo 8; valido solo per questo ordine |
| `rates[].price` | Prezzo di quel servizio |
| `shipping_price` | Prezzo di `best_rate` |

Una spedizione verso gli Stati Uniti passa per il metodo UPS con le righe articolo richieste per la dogana. Questo esempio usa la forma `packages`, che riporta gli articoli per scatola:

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10497-label" \
  -d '{
    "shipping_method": 61,
    "name": "Daniel Price",
    "telephone": "2065550117",
    "email": "daniel.price@example.com",
    "address_1": "500 Mercer St",
    "city": "Seattle",
    "province": "WA",
    "postcode": "98109",
    "country": "US",
    "package_type": "parcel",
    "paid_by": 1,
    "ref": "NB-10497",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA",
    "packages": [
      {
        "ref": "NB-10497-1",
        "weight": 2.6,
        "length": 45,
        "width": 30,
        "height": 20,
        "weight_unit": 2,
        "dimension_unit": 2,
        "items": [
          {
            "name": "Down sleeping bag",
            "description": "Down-filled sleeping bag, -7 C rating",
            "quantity": 1,
            "unit_price": 289.00,
            "currency": "CAD",
            "weight": 1.6,
            "hscode": "9404400000",
            "sku": "NB-SB-7C",
            "unit": "PCS"
          },
          {
            "name": "Camp stove",
            "description": "Canister camp stove",
            "quantity": 1,
            "unit_price": 79.00,
            "currency": "CAD",
            "weight": 1.0,
            "hscode": "7321111000",
            "sku": "NB-ST-01",
            "unit": "PCS"
          }
        ]
      }
    ]
  }'
```

**GraphQL:** `labelserviceSubmitOrder` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)). Il corpo REST va in `input`:

```graphql
mutation {
  labelserviceSubmitOrder(input: {
    shipping_method: 59
    name: "Emily Tremblay"
    telephone: "4035550182"
    address_1: "1415 17 Ave SW"
    city: "Calgary"
    province: "AB"
    postcode: "T2T0C8"
    country: "CA"
    weight: 4.2
    length: 60
    width: 30
    height: 25
    dimension_unit: 2
    weight_unit: 2
    package_type: "parcel"
    ref: "NB-10482"
    sender_name: "Northbound Outfitters"
    sender_telephone: "4165550140"
    sender_address_1: "1200 Eglinton Ave E"
    sender_city: "Toronto"
    sender_province: "ON"
    sender_postcode: "M3C1H9"
    sender_country: "CA"
  })
}
```

**Verifica:** la risposta contiene un `id` e almeno un `rates[].rate_id`. Memorizzateli entrambi. Lo stesso `Idempotency-Key` con lo stesso corpo restituisce lo stesso `id` e non crea un secondo ordine.

## 8. Acquistare l’etichetta e leggere il dettaglio della spedizione

Questa chiamata acquista l’etichetta al servizio scelto, la addebita e restituisce i numeri di tracking del corriere. Quando l’etichetta è già acquistata, legge soltanto il dettaglio, quindi una chiamata ripetuta non acquista mai due volte. Northbound Outfitters invia il `rate_id` del servizio pagato dal cliente.

**REST:** `POST /api/v1/labelservice/getShippingDetail` — [Manuale REST](/api/documentation#/paths/v1-labelservice-getShippingDetail/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingDetail \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "128455",
    "type": "ORDER_ID",
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  }'
```

```json
{
  "id": 128455,
  "shippingPrice": "24.86",
  "mainTrackingNumber": "7023210039414604",
  "trackingNumber": "7023210039414604",
  "needSubmitShippingInformation": false,
  "rate": {
    "carrier_name": "canadapost",
    "price": 24.86,
    "price_detail": [
      { "name": "Base charge", "amount": 18.40 },
      { "name": "Fuel surcharge", "amount": 3.60 },
      { "name": "GST", "amount": 1.10 }
    ],
    "tax_items": ["HST", "GST", "PST", "QST"]
  },
  "labelStatus": "ready",
  "shippingLabel": "JVBERi0xLjQKMS... (base64 encoded)"
}
```

| Campo | Uso |
|---|---|
| `mainTrackingNumber` | Numero di tracking del corriere del primo collo; comunicatelo al cliente |
| `trackingNumber` | Numeri di tracking del corriere di tutti i colli, separati da virgole |
| `shippingPrice` | Importo addebitato |
| `labelStatus` | `ready`: `shippingLabel` contiene il PDF. `pending`: acquistata e addebitata, il corriere non ha ancora prodotto il file; chiamate di nuovo più tardi. `failed`: il recupero in background si è interrotto; una nuova chiamata lo riavvia |
| `needSubmitShippingInformation` | `true` quando questo metodo richiede l’invio delle informazioni di spedizione (passo 13) |

`type` può essere `ORDER_ID` (predefinito), `TRACKING_NUMBER` (il numero di collo Superroute) o `THIRD_PARTY_TRACKING_NUMBER` (il numero del corriere). Inviate `rate_id` affinché l’etichetta sia acquistata al servizio scelto; in sua assenza il metodo acquista alla tariffa predefinita.

**GraphQL:** `labelserviceGetShippingDetail` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceGetShippingDetail))

```graphql
mutation {
  labelserviceGetShippingDetail(
    id: "128455"
    type: "ORDER_ID"
    rate_id: "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  )
}
```

**Verifica:** `mainTrackingNumber` non è vuoto e `labelStatus` è `ready` (oppure `pending`, che diventa `ready` in una chiamata successiva). Una seconda chiamata restituisce lo stesso numero di tracking e lo stesso `shippingPrice`.

## 9. Scaricare il PDF

Il magazzino stampa l’etichetta del corriere a partire da questa chiamata. Se l’etichetta non è ancora stata acquistata, la prima chiamata la acquista alla tariffa predefinita, come al passo 8; chiamate prima il passo 8 per fissare il servizio.

**REST:** `POST /api/v1/labelservice/getShippingLabel` — [Manuale REST](/api/documentation#/paths/v1-labelservice-getShippingLabel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "128455",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

```json
"JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+..."
```

- Con `base64: 1` il corpo è il PDF come un’unica stringa base64; decodificatela e inviatela alla stampante.
- Con `base64: 0` la risposta è il file PDF stesso (`application/pdf`).

`type` può essere `ORDER_ID` (predefinito), `TRACKING_NUMBER` o `THIRD_PARTY_TRACKING_NUMBER` (il numero del corriere). Questa è l’**etichetta ufficiale del corriere**. Il numero di pezzi è fissato dalla prenotazione.

**GraphQL:** `labelserviceGetShippingLabel` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)). GraphQL restituisce sempre la stringa base64.

**Verifica:** il PDF si apre e mostra il codice a barre / numero di tracking del corriere del passo 8. Stampate una copia di prova, poi eliminatela — non consegnate un’etichetta di prova a un corriere.

## 10. Tracciare

Il negozio mostra l’avanzamento del collo nella pagina dell’ordine del cliente. L’endpoint di tracking pubblico non richiede token e accetta il numero del corriere del passo 8.

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

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

```json
{
  "result": true,
  "is_third_party_tracking": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 430,
      "otep_status": "in_transit",
      "description": "Item in transit",
      "location_city": "Mississauga",
      "updated_at_localized": "2026-09-29 18:42"
    }
  ]
}
```

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

```graphql
query {
  trackingPublic(trackingNumber: "7023210039414604") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      otep_status
      description
      updated_at_localized
    }
    third_party_info { tracking_number status carrier_tracking_link }
    proofs { file_id type full_url signed_url }
  }
}
```

- `is_third_party_tracking` è true quando gli eventi provengono dal corriere.
- `data`: evento più recente per primo. Basate la logica su `tracking_event_status_id` / `otep_status`, non su `description`. Gli eventi iniziali possono ancora essere «informazioni inviate» finché il corriere non scansiona il collo.
- `deliveried` è true e `500` indica consegnato; `proofs[]` può allora includere firma (`type` `1`) o foto (`type` `2`).

**Verifica:** la ricerca restituisce la spedizione appena creata. Un numero sconosciuto o annullato restituisce `404` con `result: false`.

## 11. Configurare le notifiche degli eventi

I webhook sostituiscono il polling: il server del negozio riceve ogni scansione del corriere e aggiorna l’ordine senza chiamare periodicamente il passo 10.

| Impostazione | Evento | Quando |
|---|---|---|
| `tracking_event_webhook_url` | `tracking.event` | Scansioni del corriere, in consegna, consegnato |
| `order_status_change_webhook_url` | `order.status_change` | Stato nel vostro sistema |
| `order_create_webhook_url` | `order.created` | È stato creato un ordine etichetta (passo 7); inviato per gli ordini etichetta solo quando `order_created_webhook_all_types` è `1` |

**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://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

```json
{
  "result": true,
  "changed_keys": [
    "tracking_event_webhook_url",
    "order_create_webhook_url",
    "order_created_webhook_all_types",
    "webhook_sign_secret",
    "webhook_verify_ssl"
  ],
  "recipient_type": "business",
  "settings": {
    "webhook_sign_secret": "************CRET",
    "tracking_event_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_verify_ssl": 1
  }
}
```

- Vengono modificate solo le chiavi inviate; una chiave sconosciuta è rifiutata con `400`.
- `changed_keys` elenca ciò che è stato memorizzato. Il segreto è sempre restituito mascherato.

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

Ogni `tracking.event` contiene `order_id`, `tracking_event_status_id`, `tracking_event_key`, `tracking_number` e `external_tracking_number`; associatelo al vostro ordine tramite `order_id` (l’`id` del passo 7).

Verificate **v2** sul corpo grezzo: `HMAC_SHA256(timestamp + "." + raw_body, secret)` contro `X-Webhook-Signature-V2`. Eseguite la deduplicazione su `X-Webhook-Event-Id`. Rispondete **2xx in meno di 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;
}
```

`order_cancel_failed_webhook_url` (`order.cancel_failed`) non viene inviato per gli annullamenti di etichette del passo 12; un annullamento di etichetta rifiutato è segnalato nella risposta di quella chiamata.

**Verifica:** un `submitOrder` di prova produce `order.created` con l’`id` dell’ordine, e la prima scansione del corriere produce `tracking.event`. Una firma non valida deve essere rifiutata dal ricevitore con `401`.

## 12. Annullare

Un’etichetta che non verrà spedita si annulla affinché il corriere non la fatturi; l’addebito viene rimborsato sull’account. L’annullamento è possibile solo finché il corriere lo consente ancora (di solito prima del ritiro).

**REST:** `POST /api/v1/labelservice/cancelShippingLabel` — [Manuale REST](/api/documentation#/paths/v1-labelservice-cancelShippingLabel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/cancelShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10482-cancel" \
  -d '{"id": 128455}'
```

```json
{
  "result": true,
  "message": "Shipping Label cancelled successfully"
}
```

- Inviate esattamente uno tra `id` (l’id dell’ordine) e `tracking_number` (il numero di tracking Superroute o del corriere). L’invio di entrambi restituisce `400`.
- `result: true`: il corriere ha accettato l’annullamento e l’addebito dell’etichetta è stato rimborsato.

**GraphQL:** `labelserviceCancelShippingLabel` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceCancelShippingLabel)).

Un corriere che ha già il collo rifiuta: la risposta è `400` con `result: false` e il messaggio del corriere. Un ordine la cui etichetta non è mai stata acquistata non può essere annullato con questa chiamata.

**Verifica:** la risposta è `result: true` e il tracking pubblico per quel numero restituisce `404`. Un nuovo tentativo con lo stesso `Idempotency-Key` restituisce la risposta memorizzata; una nuova richiesta di annullamento per lo stesso ordine restituisce `400` `This order already cancelled`.

## 13. Inviare le informazioni di spedizione e chiudere la giornata (solo se questo metodo lo richiede)

Alcuni corrieri richiedono la trasmissione delle spedizioni della giornata (un manifesto) prima del ritiro. Il passo 8 lo indica per ogni ordine in `needSubmitShippingInformation`. Raccogliete gli id di tali ordini durante la giornata e inviateli dopo l’ultima etichetta.

**REST:** `POST /api/v1/labelservice/submitShippingInformation` — [Manuale REST](/api/documentation#/paths/v1-labelservice-submitShippingInformation/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitShippingInformation \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": [128455, 128461, 128470]}'
```

```json
{
  "result": true,
  "message": "Processed 3 orders. Success: 3, Failed: 0",
  "data": {
    "total_processed": 3,
    "success_count": 3,
    "failure_count": 0,
    "details": [
      { "order_id": 128455, "result": true, "message": "Successful" },
      { "order_id": 128461, "result": true, "message": "Successful" },
      { "order_id": 128470, "result": true, "message": "Successful" }
    ]
  }
}
```

- `details[]`: una riga per ordine; inviate di nuovo gli ordini con `result: false` dopo aver corretto la causa indicata in `message`.
- `404` `No eligible orders found for shipping information submission`: nessuno degli id ha un’etichetta acquistata che richieda ancora l’invio.

**GraphQL:** `labelserviceSubmitShippingInformation` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceSubmitShippingInformation)).

Chiudete poi la giornata. La chiamata non ha corpo e copre tutti gli ordini etichetta del chiamante.

**REST:** `POST /api/v1/labelservice/endofday` — [Manuale REST](/api/documentation#/paths/v1-labelservice-endofday/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/endofday \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "success": 0,
  "failed": 0,
  "success_ids": [],
  "failed_ids": []
}
```

- `400` con `There are orders need to submit shipping information`: alcune etichette acquistate richiedono ancora l’invio; inviatele con `submitShippingInformation` e chiamate di nuovo.

**GraphQL:** `labelserviceEndofday` ([Manuale GraphQL](/api/graphql/documentation#/labelservice/labelserviceEndofday)).

Saltate questo passo quando nessun ordine della giornata ha riportato `needSubmitShippingInformation: true`.

**Verifica:** `submitShippingInformation` riporta `failure_count: 0` e `endofday` risponde `200`. Eseguitelo prima su un metodo di prova.

## 14. Gestione degli errori

Gli errori del servizio etichette contengono un `message`; un `code` è presente solo dove la tabella lo indica.

| Situazione | Stato HTTP | Codice | Cosa fa l’integrazione |
|---|---|---|---|
| Token mancante o scaduto, oppure accesso API non abilitato | `401` | — (`Unauthorized`) | Eseguite di nuovo l’accesso; se il problema persiste, chiedete all’azienda di abilitare l’accesso API |
| `shipping_method` mancante o non disponibile per il chiamante | `400` | — | Ricaricate l’elenco dei metodi (passo 5) e usate un `id` presente in esso |
| `package_type` non offerto dal metodo | `400` | — | Usate una chiave di `package_type` del passo 5 |
| Indirizzo o collo non valido, oppure il corriere non restituisce alcuna tariffa | `400` | — (messaggio del corriere) | Mostrate il messaggio, correggete i dati e quotate di nuovo |
| `auto_deduplication` è `1` e il `ref` esiste già | `400` | — (`exist_order_ids`) | Usate l’ordine esistente indicato in `exist_order_ids` invece di crearne uno nuovo |
| Stesso `Idempotency-Key` con un corpo diverso | `409` | `IDEMPOTENCY_CONFLICT` | Usate una nuova chiave per una richiesta diversa |
| Stesso `Idempotency-Key` mentre la prima richiesta è ancora in corso | `409` | `IDEMPOTENCY_IN_PROGRESS` | Attendete `Retry-After` secondi e riprovate con la stessa chiave e lo stesso corpo |
| Saldo più credito del cliente non coprono l’etichetta | `400` | `INSUFFICIENT_BALANCE` | Ricaricate il saldo in base al dettaglio `insufficient_balance` (`shortfall`, `add_funds_url`) e chiamate di nuovo il passo 8 |
| Etichetta acquistata, file del corriere non ancora pronto | `400` al primo acquisto, `200` in seguito | `shipment_label_not_ready` | Attendete finché `labelStatus` è `pending`; chiamate di nuovo il passo 8 quando è `failed` |
| Id o numero d’ordine non appartenente al chiamante | `401` | — (`Not Auth`) | Verificate l’id e il `type`; usate l’account che ha creato l’ordine |
| Annullamento rifiutato dal corriere, oppure ordine già annullato | `400` | — | Considerate l’etichetta come spedita (o già annullata); non riprovate |
| Numero di tracking sconosciuto o annullato | `404` | — | Smettete di mostrare la cronologia per quel numero |
| `endofday` con spedizioni non ancora inviate | `400` | — | Eseguite `submitShippingInformation` per quegli ordini, poi chiamate di nuovo |

## Elenco di verifica

Usate una destinazione che controllate e un metodo annullabile:

- [ ] L’elenco dei metodi non è vuoto; avete annotato un `id`.
- [ ] La quotazione restituisce un prezzo per quel metodo e quella destinazione.
- [ ] La creazione restituisce un `id` d’ordine e `rates[].rate_id`; lo stesso `Idempotency-Key` non crea un secondo ordine.
- [ ] `getShippingDetail` con il `rate_id` scelto restituisce `mainTrackingNumber`; una seconda chiamata non addebita di nuovo.
- [ ] Il PDF dell’etichetta si apre e mostra il numero di tracking del corriere.
- [ ] Il tracking pubblico trova la spedizione con quel numero.
- [ ] Arriva `tracking.event` (e `order.created`, se abilitato); la firma v2 viene verificata.
- [ ] Una spedizione transfrontaliera di prova con `items` viene accettata dal corriere.
- [ ] L’annullamento riesce, **oppure** avete confermato che questo metodo non può essere annullato dopo la prenotazione.
- [ ] Se il metodo richiede la chiusura di fine giornata, un’esecuzione di prova termina senza errori.
