# Preventivo e ordine in un unico flusso

Questa guida illustra l’API Uniorder (`/api/v1/uniorder/...`) richiesta per richiesta, nell’ordine in cui si costruisce un’integrazione: autenticarsi, richiedere un preventivo, creare l’ordine al `rate_id` scelto, stampare l’etichetta, leggere, tracciare e annullare l’ordine, ed elaborare le spedizioni in lotti. Un preventivo elenca tutti i modi in cui l’account può spedire un collo: la consegna da parte dell’azienda stessa e, su richiesta, tutti i servizi etichetta dei corrieri. Ordinare con un `rate_id` crea l’ordine per quel servizio: un ordine di consegna, oppure un ordine etichetta con l’etichetta acquistata presso il servizio del corriere indicato nel preventivo. È destinata agli sviluppatori di negozi online, sistemi di gestione degli ordini ed ERP che spediscono tramite un account aziendale.

## 1. Cosa potete realizzare

Gli esempi seguenti riguardano un’unica azienda: **Fleurs du Plateau**, un fiorista in 4500 Rue Saint-Denis, Montreal (H2J 2L3), che vende bouquet online. Un collo tipico è una scatola da 1,2 kg di 40 × 25 × 25 cm, destinata a Jane Recipient in 6841 Rue Saint-Denis, Montreal (H2S 2S3), con l’ordine web `WEB-10045`.

- **Un checkout che offre tutte le opzioni di spedizione.** Il negozio richiede un unico preventivo per il collo e mostra la consegna locale in giornata accanto a tutti i servizi etichetta dei corrieri dell’account, ciascuno con il proprio prezzo, quindi crea l’ordine con l’opzione scelta dal cliente.
- **Stampa automatica delle etichette.** Alla creazione dell’ordine, il negozio scarica il PDF dell’etichetta e lo invia alla stampante della postazione di imballaggio, sia che il collo venga consegnato dall’azienda sia da un corriere.
- **Una pagina ordine con tracciamento in tempo reale.** La pagina ordine del cliente mostra lo stato e la cronologia degli eventi della spedizione, con la prova di consegna una volta consegnato il bouquet.
- **Un lotto notturno dall’ERP.** Gli ordini all’ingrosso della giornata vengono quotati e creati in un unico job in coda di al massimo 500 righe, e ogni risultato viene ricondotto alla propria riga d’ordine tramite `reference`.

## 2. Contenuto di questa guida

Questa è la guida passo per passo dell’API Uniorder. La panoramica di ciò che Uniorder offre, e del perché, si trova in **Uniorder: un'API per ogni spedizione**; questa guida riporta le richieste, le risposte e le verifiche di ciascuna chiamata.

Uniorder è il punto di accesso unico raccomandato per le nuove integrazioni che spediscono colli con consegna locale o con etichetta del corriere: sostituisce le chiamate separate all’API di consegna locale e all’API delle etichette corriere con un’unica struttura di richiesta. Gli endpoint precedenti descritti in **Ritiro e consegna (flotta propria)** e **Etichette corriere** restano disponibili e invariati. Uniorder non si applica ai servizi di spedizione prenotati da un account cliente né agli ordini di stoccaggio e uscita; per questi usate **Servizi di spedizione** e **Stoccaggio e uscita**.

## 3. Prima di iniziare

- **Account.** Usate un account aziendale (cliente), oppure un account dipendente dell’azienda, con autorizzazione API. Anche un account cliente dell’azienda può chiamare Uniorder e viene sempre quotato e fatturato come sé stesso. La creazione di un ordine di consegna richiede l’autorizzazione a effettuare ordini.
- **Clienti.** Un account cliente o dipendente può richiedere preventivi e ordinare per uno dei propri clienti con `customer_id` o `customer_code` nel preventivo; il `rate_id` porta quindi con sé quel cliente, e il prezzo segue il piano del cliente.
- **Servizi etichetta.** Per ricevere tariffe `label_service`, l’account (o il cliente indicato) deve avere almeno un account corriere per etichette configurato.
- **Dati di prova.** Usate un indirizzo all’interno dell’area di consegna dell’azienda per le tariffe `self_delivery`, e riferimenti di prova come `WEB-10045` che possano essere annullati in seguito.
- **Token.** Richiedete il token di accesso dal vostro server e conservatelo lì. Non inviatelo mai a un browser o a un’app mobile.
- **Segnaposto.** Sostituite `YOUR_HOST` con l’host API del vostro ambiente e `ACCESS_TOKEN` con il token del passo 4.

## 4. Autenticarsi

Ogni chiamata Uniorder viene eseguita per conto di un account. Effettuate il login una volta dal vostro server, conservate il token restituito e inviatelo in ogni richiesta.

**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":"orders@fleursduplateau.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

- `access_token`: inseritelo nell’intestazione di ogni richiesta successiva:

```
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. Quotare tutti i servizi

Il preventivo elenca tutti i modi in cui il collo può essere spedito, con un prezzo e un `rate_id` per ciascuno. Il checkout mostra le tariffe come opzioni; non viene creato né prenotato nulla.

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

Il mittente e il destinatario sono indirizzi completi; solo `from_address_2` e `to_address_2` sono facoltativi. Ogni collo richiede `weight`, `length`, `width` e `height`. Impostate `quote_labels` su `true` per aggiungere i servizi etichetta dei corrieri; in tal caso nomi e telefoni di entrambe le parti sono obbligatori. Un account cliente o dipendente può richiedere un preventivo per uno dei propri clienti con `customer_id` o `customer_code`. Una finestra di consegna (`time_window_start`, `time_window_end`, formato `YYYY-MM-DD HH:MM:SS`) viene considerata quando il prezzo dipende da essa.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

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

```json
{
  "result": true,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`: consegna da parte dell’azienda. Al massimo una per preventivo.
- `type` `label_service`: una per ogni servizio di ciascun account etichette. Mostrate al cliente `service_name`, `shipping_price` e `transit_days`.
- `errors` elenca ciò che non è stato possibile quotare, con il relativo `type`. Un indirizzo fuori dall’area di consegna è un errore di tipo `self_delivery` con codice `OUT_OF_DELIVERY_AREA`; mostrate solo i servizi etichetta.
- `rate_id` è valido per 30 minuti e solo per l’account che ha richiesto il preventivo. Conservatelo con la sessione di checkout.
- `result` è `true` quando è stata trovata almeno una tariffa.

**GraphQL:** `uniorderRate` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderRate)). La risposta è uno scalare JSON, quindi l’operazione non ha selection set.

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

Variabili:

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verifica:** `rates` contiene una tariffa `self_delivery` per un indirizzo in zona e, con `quote_labels`, una tariffa `label_service` per ogni servizio del corriere. Non viene creato nulla.

## 6. Creare l’ordine alla tariffa scelta

Quando il cliente paga, il negozio crea l’ordine con il `rate_id` dell’opzione scelta e la stessa spedizione. Il `rate_id` determina il servizio; nient’altro nella richiesta lo seleziona.

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

Inviate in ogni creazione un’intestazione `Idempotency-Key`, univoca per ordine. Un nuovo tentativo con la stessa chiave e lo stesso corpo restituisce la prima risposta con `replayed` `true` e non crea un secondo ordine.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

Una tariffa `self_delivery` crea un ordine di consegna. Per `type` `D` il destinatario è la fermata; impostate `need_pick_up` su `1` per far ritirare il collo presso il mittente. Per `type` `P` il mittente è la fermata.

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

Una tariffa `label_service` crea un ordine etichetta e acquista l’etichetta presso il servizio del corriere indicato nel preventivo. `type` deve essere `D`, e `from_name`, `from_telephone`, `to_name` e `to_telephone` sono obbligatori. Se il cliente avesse scelto UPS STANDARD, la risposta sarebbe:

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`: conservatelo con l’ordine web; ogni chiamata successiva lo utilizza.
- `tracking_numbers`: i numeri di tracciamento propri della spedizione, uno per collo.
- `shipping_price`: il prezzo addebitato. L’ordine viene prezzato alla creazione; `quoted_price` è il prezzo del preventivo. I due possono differire.
- `label.main_tracking_number` e `label.shipping_label` (solo ordine etichetta): il numero di tracciamento del corriere e il PDF dell’etichetta in base64.
- `result` `false` con codice `LABEL_PURCHASE_FAILED` (solo ordine etichetta): l’ordine esiste ma non ha un’etichetta. Conservate l’`id` e proseguite con il passo 11.

**GraphQL:** `uniorderCreate` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderCreate))

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

Variabili:

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verifica:** `result` è `true` e `id` è valorizzato. Un `rate_id` scaduto o appartenente a un altro account restituisce `400` con codice `RATE_ID_INVALID`, e non viene creato nulla.

## 7. Stampare l’etichetta

La postazione di imballaggio stampa l’etichetta non appena l’ordine esiste. La stessa chiamata restituisce l’etichetta propria dell’azienda per un ordine di consegna e l’etichetta del corriere acquistata per un ordine etichetta.

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

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`: il PDF dell’etichetta in base64. Decodificatelo e inviate il file alla stampante.
- `hide_sender_address`, `hide_receiver_address` (`1` per nascondere): si applicano all’etichetta propria dell’azienda di un ordine di consegna.
- `label_status` (ordine etichetta): `ready` quando il file viene restituito. Quando il corriere non ha ancora generato il file, la risposta è `200` con `result` `false` e `label_status` `pending`; richiedete di nuovo l’etichetta più tardi.
- Questa chiamata non acquista mai un’etichetta: un’etichetta non ancora acquistata restituisce `409` con il codice `LABEL_PURCHASE_FAILED`. Acquistatela con il passo 11.

**GraphQL:** `uniorderLabel` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderLabel))

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**Verifica:** `result` è `true` e il `pdf_data` decodificato si apre come PDF che mostra il numero di tracciamento dell’ordine.

## 8. Leggere l’ordine

Il negozio legge l’ordine per mostrarne lo stato, gli indirizzi e i colli nella pagina ordine o in una schermata del servizio clienti.

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

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`: `self_delivery` o `label_service`; gli altri campi hanno la stessa struttura per entrambi.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` o `cancelled` per un ordine di consegna, e `label_pending`, `label_purchased` o `cancelled` per un ordine etichetta.
- `label` (solo ordine etichetta): il corriere, il servizio, `carrier_tracking_numbers` e `label_status` (`not_purchased`, `pending`, `ready` o `failed`).

**GraphQL:** `uniorder` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorder))

```graphql
query {
  uniorder(order_id: 123456)
}
```

**Verifica:** l’ordine restituisce il proprio `status` e i propri `packages`, e `ref` coincide con l’ordine web.

## 9. Tracciare l’ordine

La pagina ordine mostra la cronologia della spedizione. Leggetela quando il cliente apre la pagina, oppure mantenetela aggiornata tramite i webhook.

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

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`: la cronologia, dalla più recente, ciascun evento con `code`, `description`, `location` e ora.
- `proofs`: i file di prova di consegna. Mostrateli quando `status` è `delivered`.
- `carrier` (solo ordine etichetta): il nome del corriere, il numero di tracciamento e il link di tracciamento (`tracking_url`).

**GraphQL:** `uniorderTracking` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderTracking))

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**Verifica:** la chiamata di tracciamento restituisce `result` `true`, lo `status` dell’ordine e i suoi `events`.

## 10. Annullare l’ordine

Quando il cliente annulla l’ordine web, il negozio annulla la spedizione con la stessa chiamata per un ordine di consegna e per un ordine etichetta. Un’etichetta viene prima annullata presso il suo corriere.

**REST:** `POST /api/v1/uniorder/{orderId}/cancel` — [Manuale REST](/api/documentation#/paths/v1-uniorder-orderId--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`: `true` quando l’ordine era stato annullato prima di questa chiamata. Consideratelo un esito positivo.
- Quando l’ordine non viene annullato, la risposta è `409` e l’ordine resta invariato: `ORDER_STATUS_NOT_CANCELLABLE` (troppo tardi per annullare), `ORDER_CANCEL_REFUSED` (non annullabile al momento) o `LABEL_CANCEL_FAILED` (il corriere non ha annullato l’etichetta). Mantenete aperto l’ordine web e gestite la spedizione manualmente.

**GraphQL:** `uniorderCancel` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderCancel))

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**Verifica:** `result` è `true`. Annullare di nuovo lo stesso ordine restituisce `already_cancelled` `true`.

## 11. Acquistare un’etichetta in seguito (solo dopo LABEL_PURCHASE_FAILED)

Questo passo si applica solo a un ordine etichetta la cui creazione ha risposto `LABEL_PURCHASE_FAILED`. La risposta era `200` con `result` `false`, il codice `LABEL_PURCHASE_FAILED` e l’`id` dell’ordine: l’ordine viene conservato senza etichetta. Non inviate di nuovo l’ordine; acquistate l’etichetta per quell’ordine.

**REST:** `POST /api/v1/uniorder/{orderId}/label` — [Manuale REST](/api/documentation#/paths/v1-uniorder-orderId--label/post)

La creazione che non è riuscita ad acquistare l’etichetta ha risposto:

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

Acquistate l’etichetta per l’ordine `123458`:

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

L’etichetta viene acquistata presso il servizio scelto alla creazione dell’ordine. Per acquistarla presso un altro servizio dello stesso account, inviate nel corpo un nuovo `rate_id` `label_service` dal passo 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Un’etichetta già acquistata viene restituita e non viene acquistata di nuovo.

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`: il PDF dell’etichetta in base64; stampatelo come al passo 7.
- `result` `false` di nuovo con `LABEL_PURCHASE_FAILED`: il corriere ha rifiutato ancora. Riprovate più tardi oppure acquistate presso un altro servizio con un nuovo `rate_id`.

**GraphQL:** `uniorderPurchaseLabel` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderPurchaseLabel))

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**Verifica:** `result` è `true` e `label.shipping_label` contiene il PDF, oppure `label.label_status` è `pending` mentre il corriere genera il file.

## 12. Lotti

I lotti quotano o creano molte spedizioni in una sola chiamata, ad esempio gli ordini all’ingrosso dell’ERP. Ogni riga passa per la chiamata singola e restituisce ciò che quella chiamata restituirebbe; una riga che fallisce non ferma le altre righe.

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

Fino a 20 righe per chiamata, con risposta nella stessa chiamata: `shipments` per il lotto di preventivi, `orders` per il lotto di creazione. Ogni riga ha gli stessi campi della chiamata singola, più un `reference` facoltativo che viene restituito con il suo risultato.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`: uno per riga, con l’`index` della riga, il suo `reference`, e lo `status` e il `body` che restituirebbe la chiamata singola. Ricollegate ogni risultato alla sua riga d’ordine tramite `reference`.

**REST:** `POST /api/v1/uniorder/rate/batch-async` — [Manuale REST](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [Manuale REST](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [Manuale REST](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

Fino a 500 righe, messe in coda come un unico job. La chiamata restituisce un `job_id`; leggete il job finché `status` è `done`, poi leggete `results`. Lo stesso lotto inviato di nuovo mentre il primo è ancora in coda restituisce il primo job con `duplicate` `true`.

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`: `queued`, `done`, oppure `failed` con un `message` quando il job non ha potuto essere elaborato.
- Un job viene eseguito una volta e non viene ripetuto. Un `rate_id` che scade prima dell’elaborazione della sua riga restituisce `RATE_ID_INVALID` per quella riga; inviate il job di creazione poco dopo il completamento del job di preventivo.

**GraphQL:** `uniorderRateBatch` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderRateBatch)) · `uniorderCreateBatch` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderCreateBatch)) · `uniorderRateBatchAsync` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderRateBatchAsync)) · `uniorderCreateBatchAsync` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)) · `uniorderJob` ([Manuale GraphQL](/api/graphql/documentation#/orders/uniorderJob))

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**Verifica:** un lotto restituisce un risultato per riga; un job asincrono raggiunge `status` `done`.

## 13. Gestione degli errori

| Situazione | Stato HTTP | Codice | Cosa fa l’integrazione |
|---|---|---|---|
| Un campo obbligatorio manca o non è valido | 400 | `VALIDATION_FAILED` | Correggete il campo indicato in `message` e inviate di nuovo la richiesta. |
| Il destinatario è fuori dall’area di consegna (preventivo) | 200 | `OUT_OF_DELIVERY_AREA` in `errors` | Offrite solo le tariffe `label_service`. |
| Il `rate_id` è scaduto, non è valido o appartiene a un altro account | 400 | `RATE_ID_INVALID` | Richiedete un nuovo preventivo e create l’ordine con il suo `rate_id`. Non è stato creato nulla. |
| L’ordine etichetta è stato creato ma la sua etichetta non è stata acquistata | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Conservate l’`id`; acquistate l’etichetta con `POST /api/v1/uniorder/{orderId}/label`. Non create mai di nuovo l’ordine. |
| L’etichetta viene richiesta prima di essere stata acquistata | 409 | `LABEL_PURCHASE_FAILED` | Acquistate l’etichetta con `POST /api/v1/uniorder/{orderId}/label`. |
| L’ordine è troppo avanzato per essere annullato | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Lasciate l’ordine com’è; gestite il reso separatamente. |
| L’ordine non può essere annullato al momento | 409 | `ORDER_CANCEL_REFUSED` | Lasciate l’ordine com’è; riprovate più tardi o contattate l’azienda. |
| Il corriere non ha annullato l’etichetta | 409 | `LABEL_CANCEL_FAILED` | L’ordine resta invariato; riprovate l’annullamento più tardi. |
| L’ordine o il job non esiste o appartiene a un altro account | 404 | `ORDER_NOT_FOUND` | Controllate l’`id` conservato con l’ordine web. |
| Un `Idempotency-Key` viene riutilizzato con un corpo diverso | 409 | `IDEMPOTENCY_CONFLICT` | Usate una nuova chiave per una richiesta diversa. |
| Il token manca o è scaduto, oppure l’account non può effettuare ordini | 401 | — | Effettuate di nuovo il login; controllate le autorizzazioni dell’account. |

## Elenco di verifica

Usate un `ref` di prova come `WEB-10045`:

- [ ] Il preventivo restituisce una tariffa `self_delivery` per un indirizzo in zona.
- [ ] Con `quote_labels`, il preventivo restituisce tariffe `label_service`, ciascuna con un `rate_id`.
- [ ] Ordinare con un `rate_id` `self_delivery` restituisce `id` e `tracking_numbers`.
- [ ] Ordinare con un `rate_id` `label_service` restituisce l’etichetta del servizio indicato nel preventivo.
- [ ] Lo stesso `Idempotency-Key` non crea un secondo ordine.
- [ ] Un `rate_id` più vecchio di 30 minuti restituisce `RATE_ID_INVALID`.
- [ ] L’etichetta di ogni ordine si decodifica in un PDF stampabile.
- [ ] L’ordine, la sua etichetta e il suo tracciamento si possono leggere con l’`id` restituito alla creazione.
- [ ] L’annullamento di un ordine di prova restituisce `result: true`; un secondo annullamento restituisce `already_cancelled: true`.
- [ ] Dopo `LABEL_PURCHASE_FAILED`, `POST /api/v1/uniorder/{orderId}/label` acquista l’etichetta per lo stesso ordine.
- [ ] Un lotto di due righe restituisce due risultati con il loro `reference`.
