# Devis et commande en un seul parcours

Ce guide présente l’API Uniorder (`/api/v1/uniorder/...`) requête par requête, dans l’ordre dans lequel une intégration se construit : authentification, devis, création au `rate_id` choisi, impression de l’étiquette, consultation, suivi et annulation de la commande, puis traitement des envois par lots. Un seul devis liste toutes les façons dont le compte peut expédier un colis : la livraison par l’entreprise elle-même et, sur demande, tous les services d’étiquette des transporteurs. Commander avec un `rate_id` crée la commande pour ce service : une commande de livraison, ou une commande d’étiquette avec l’étiquette achetée auprès du service transporteur figurant dans le devis. Il s’adresse aux développeurs de boutiques en ligne, de systèmes de gestion des commandes et d’ERP qui expédient via un compte professionnel.

## 1. Ce que vous pouvez construire

Les exemples ci-dessous suivent une seule entreprise : **Fleurs du Plateau**, un fleuriste situé au 4500 Rue Saint-Denis, Montréal (H2J 2L3), qui vend des bouquets en ligne. Un colis type est une boîte de 1,2 kg de 40 × 25 × 25 cm, destinée à Jane Recipient au 6841 Rue Saint-Denis, Montréal (H2S 2S3), sous la commande web `WEB-10045`.

- **Un paiement qui propose toutes les options d’expédition.** La boutique demande un devis une seule fois pour le colis et affiche la livraison locale le jour même à côté de chaque service d’étiquette transporteur du compte, chacun avec son prix, puis crée la commande avec l’option choisie par le client.
- **Impression automatique des étiquettes.** Lorsque la commande est créée, la boutique télécharge le PDF de l’étiquette et l’envoie à l’imprimante du poste d’emballage, que le colis soit livré par l’entreprise ou par un transporteur.
- **Une page de commande avec suivi en direct.** La page de commande du client affiche le statut et la chronologie des événements de l’envoi, avec la preuve de livraison une fois le bouquet livré.
- **Un lot nocturne depuis l’ERP.** Les commandes de gros de la journée sont tarifées et créées dans une seule tâche en file d’attente comptant jusqu’à 500 lignes, et chaque résultat est rapproché de sa ligne de commande par `reference`.

## 2. Ce que couvre ce guide

Ceci est le guide pas à pas de l’API Uniorder. La présentation de ce qu’offre Uniorder, et pourquoi, figure dans **Uniorder : une API pour chaque envoi** ; ce guide fournit les requêtes, les réponses et les vérifications de chaque appel.

Uniorder est le point d’entrée unique recommandé pour les nouvelles intégrations qui expédient des colis par livraison locale ou par étiquette transporteur : il remplace les appels distincts à l’API de livraison locale et à l’API d’étiquettes transporteur par une seule forme de requête. Les points de terminaison antérieurs décrits dans **Enlèvement et livraison (flotte propre)** et **Étiquettes transporteur** restent disponibles et inchangés. Uniorder ne s’applique ni aux services d’expédition réservés par un compte client, ni aux commandes de stockage et d’expédition ; utilisez pour ceux-ci **Services d’expédition** et **Stockage et expédition**.

## 3. Avant de commencer

- **Compte.** Utilisez un compte professionnel (client), ou un compte employé de l’entreprise, disposant de l’autorisation API. Un compte client final de l’entreprise peut également appeler Uniorder ; il est toujours tarifé et facturé en son propre nom. La création d’une commande de livraison requiert l’autorisation de passer des commandes.
- **Clients.** Un compte client ou employé peut demander un devis et commander pour l’un de ses clients avec `customer_id` ou `customer_code` dans le devis ; le `rate_id` porte alors ce client, et le prix suit le plan tarifaire du client.
- **Services d’étiquette.** Pour recevoir des tarifs `label_service`, le compte (ou le client désigné) doit disposer d’au moins un compte transporteur d’étiquettes configuré.
- **Données de test.** Utilisez une adresse située dans la zone de livraison de l’entreprise pour obtenir des tarifs `self_delivery`, et des références de test telles que `WEB-10045` qui peuvent être annulées ensuite.
- **Jetons.** Demandez le jeton d’accès depuis votre serveur et conservez-le sur celui-ci. Ne l’envoyez jamais à un navigateur ni à une application mobile.
- **Valeurs à remplacer.** Remplacez `YOUR_HOST` par l’hôte API de votre environnement et `ACCESS_TOKEN` par le jeton obtenu à l’étape 4.

## 4. Authentification

Chaque appel Uniorder est effectué au nom d’un compte. Connectez-vous une fois depuis votre serveur, conservez le jeton renvoyé et envoyez-le avec chaque requête.

**REST :** `POST /api/v1/user/login` — [Manuel 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` : placez-le dans l’en-tête de chaque requête ultérieure :

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL utilise le même en-tête sur `POST /api/graphql`.

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

**Vérification :** la connexion renvoie `access_token`. Les requêtes ultérieures sans ce jeton renvoient `401`.

## 5. Tarifer tous les services

Le devis liste toutes les façons d’expédier le colis, avec un prix et un `rate_id` pour chacune. Le paiement affiche les tarifs sous forme d’options ; rien n’est créé ni réservé.

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

L’expéditeur et le destinataire sont des adresses complètes ; seuls `from_address_2` et `to_address_2` sont facultatifs. Chaque colis requiert `weight`, `length`, `width` et `height`. Définissez `quote_labels` à `true` pour ajouter les services d’étiquette des transporteurs ; les noms et les téléphones des deux extrémités sont alors obligatoires. Un compte client ou employé peut demander un devis pour l’un de ses clients avec `customer_id` ou `customer_code`. Une fenêtre de livraison (`time_window_start`, `time_window_end`, format `YYYY-MM-DD HH:MM:SS`) est prise en compte lorsque le prix en dépend.

```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` : livraison par l’entreprise. Au plus un par devis.
- `type` `label_service` : un par service de chaque compte d’étiquettes. Affichez `service_name`, `shipping_price` et `transit_days` au client.
- `errors` liste ce qui n’a pas pu être tarifé, avec son `type`. Une adresse hors de la zone de livraison est une erreur de type `self_delivery` avec le code `OUT_OF_DELIVERY_AREA` ; affichez uniquement les services d’étiquette.
- `rate_id` est valable 30 minutes et uniquement pour le compte qui a demandé le devis. Conservez-le avec la session de paiement.
- `result` vaut `true` lorsqu’au moins un tarif a été trouvé.

**GraphQL :** `uniorderRate` ([Manuel GraphQL](/api/graphql/documentation#/orders/uniorderRate)). La réponse est un scalaire JSON ; l’opération n’a donc pas d’ensemble de sélection.

```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
  )
}
```

Variables :

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

**Vérification :** `rates` contient un tarif `self_delivery` pour une adresse dans la zone et, avec `quote_labels`, un tarif `label_service` par service transporteur. Rien n’est créé.

## 6. Créer la commande au tarif choisi

Lorsque le client paie, la boutique crée la commande avec le `rate_id` de l’option choisie et le même envoi. Le `rate_id` détermine le service ; aucun autre élément de la requête ne le sélectionne.

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

Envoyez un en-tête `Idempotency-Key`, unique par commande, à chaque création. Une nouvelle tentative avec la même clé et le même corps renvoie la première réponse avec `replayed` `true` et ne crée pas de seconde commande.

```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
    }]
  }'
```

Un tarif `self_delivery` crée une commande de livraison. Pour `type` `D`, le destinataire est l’arrêt ; définissez `need_pick_up` à `1` pour que le colis soit enlevé chez l’expéditeur. Pour `type` `P`, l’expéditeur est l’arrêt.

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

Un tarif `label_service` crée une commande d’étiquette et achète l’étiquette auprès du service transporteur figurant dans le devis. `type` doit être `D`, et `from_name`, `from_telephone`, `to_name` et `to_telephone` sont obligatoires. Si le client avait choisi UPS STANDARD, la réponse serait :

```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` : conservez-le avec la commande web ; tous les appels ultérieurs l’utilisent.
- `tracking_numbers` : les numéros de suivi propres à l’envoi, un par colis.
- `shipping_price` : le prix facturé. La commande est tarifée lors de sa création ; `quoted_price` est le prix du devis. Les deux peuvent différer.
- `label.main_tracking_number` et `label.shipping_label` (commande d’étiquette uniquement) : le numéro de suivi du transporteur et le PDF de l’étiquette en base64.
- `result` `false` avec le code `LABEL_PURCHASE_FAILED` (commande d’étiquette uniquement) : la commande existe mais n’a pas d’étiquette. Conservez l’`id` et poursuivez à l’étape 11.

**GraphQL :** `uniorderCreate` ([Manuel 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
  )
}
```

Variables :

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

**Vérification :** `result` vaut `true` et `id` est renseigné. Un `rate_id` expiré ou appartenant à un autre compte renvoie `400` avec le code `RATE_ID_INVALID`, et rien n’est créé.

## 7. Imprimer l’étiquette

Le poste d’emballage imprime l’étiquette dès que la commande existe. Le même appel renvoie l’étiquette propre de l’entreprise pour une commande de livraison et l’étiquette transporteur achetée pour une commande d’étiquette.

**REST :** `GET /api/v1/uniorder/{orderId}/label` — [Manuel 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` : le PDF de l’étiquette en base64. Décodez-le et envoyez le fichier à l’imprimante.
- `hide_sender_address`, `hide_receiver_address` (`1` pour masquer) : s’appliquent à l’étiquette propre de l’entreprise pour une commande de livraison.
- `label_status` (commande d’étiquette) : `ready` lorsque le fichier est renvoyé. Lorsque le transporteur n’a pas encore produit le fichier, la réponse est `200` avec `result` `false` et `label_status` `pending` ; demandez de nouveau l’étiquette plus tard.
- Cet appel n’achète jamais d’étiquette : une étiquette qui n’a pas été achetée renvoie `409` avec le code `LABEL_PURCHASE_FAILED`. Achetez-la avec l’étape 11.

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

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

**Vérification :** `result` vaut `true` et le `pdf_data` décodé s’ouvre comme un PDF affichant le numéro de suivi de la commande.

## 8. Consulter la commande

La boutique consulte la commande pour afficher son statut, ses adresses et ses colis sur la page de commande ou dans un écran du service client.

**REST :** `GET /api/v1/uniorder/{orderId}` — [Manuel 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` ou `label_service` ; les autres champs suivent la même forme dans les deux cas.
- `status` : `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` ou `cancelled` pour une commande de livraison, et `label_pending`, `label_purchased` ou `cancelled` pour une commande d’étiquette.
- `label` (commande d’étiquette uniquement) : le transporteur, le service, `carrier_tracking_numbers` et `label_status` (`not_purchased`, `pending`, `ready` ou `failed`).

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

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

**Vérification :** la commande renvoie son `status` et ses `packages`, et `ref` correspond à la commande web.

## 9. Suivre la commande

La page de commande affiche la chronologie de l’envoi. Consultez-la lorsque le client ouvre la page, ou tenez-la à jour à partir des webhooks.

**REST :** `GET /api/v1/uniorder/{orderId}/tracking` — [Manuel 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 chronologie, du plus récent au plus ancien, chaque événement avec `code`, `description`, `location` et l’heure.
- `proofs` : les fichiers de preuve de livraison. Affichez-les une fois que `status` vaut `delivered`.
- `carrier` (commande d’étiquette uniquement) : le nom du transporteur, le numéro de suivi et le lien de suivi (`tracking_url`).

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

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

**Vérification :** l’appel de suivi renvoie `result` `true`, le `status` de la commande et ses `events`.

## 10. Annuler la commande

Lorsque le client annule la commande web, la boutique annule l’envoi avec le même appel, qu’il s’agisse d’une commande de livraison ou d’une commande d’étiquette. Une étiquette est d’abord annulée auprès de son transporteur.

**REST :** `POST /api/v1/uniorder/{orderId}/cancel` — [Manuel 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` lorsque la commande a été annulée avant cet appel. Traitez-le comme un succès.
- Lorsque la commande n’est pas annulée, la réponse est `409` et la commande reste inchangée : `ORDER_STATUS_NOT_CANCELLABLE` (trop tard pour annuler), `ORDER_CANCEL_REFUSED` (ne peut pas être annulée pour le moment) ou `LABEL_CANCEL_FAILED` (le transporteur n’a pas annulé l’étiquette). Laissez la commande web ouverte et traitez l’envoi manuellement.

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

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

**Vérification :** `result` vaut `true`. Annuler à nouveau la même commande renvoie `already_cancelled` `true`.

## 11. Acheter une étiquette plus tard (uniquement après LABEL_PURCHASE_FAILED)

Cette étape s’applique uniquement à une commande d’étiquette dont la création a répondu `LABEL_PURCHASE_FAILED`. La réponse était `200` avec `result` `false`, le code `LABEL_PURCHASE_FAILED` et l’`id` de la commande : la commande est conservée sans étiquette. Ne soumettez pas la commande de nouveau ; achetez l’étiquette pour cette commande.

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

La création qui n’a pas pu acheter l’étiquette a répondu :

```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."
}
```

Achetez l’étiquette pour la commande `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’étiquette est achetée auprès du service choisi lors de la création de la commande. Pour l’acheter auprès d’un autre service du même compte, envoyez dans le corps un nouveau `rate_id` `label_service` issu de l’étape 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Une étiquette déjà achetée est renvoyée et n’est pas achetée de nouveau.

```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` : le PDF de l’étiquette en base64 ; imprimez-le comme à l’étape 7.
- `result` `false` avec de nouveau `LABEL_PURCHASE_FAILED` : le transporteur refuse toujours. Réessayez plus tard ou achetez auprès d’un autre service avec un nouveau `rate_id`.

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

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

**Vérification :** `result` vaut `true` et `label.shipping_label` contient le PDF, ou `label.label_status` vaut `pending` pendant que le transporteur produit le fichier.

## 12. Lots

Les lots tarifent ou créent de nombreux envois en un seul appel, par exemple les commandes de gros de l’ERP. Chaque ligne passe par l’appel unitaire et renvoie ce que cet appel renverrait ; une ligne en échec n’arrête pas les autres lignes.

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

Jusqu’à 20 lignes par appel, traitées dans la même réponse : `shipments` pour le lot de devis, `orders` pour le lot de création. Chaque ligne comporte les mêmes champs que l’appel unitaire, plus une `reference` facultative qui est renvoyée avec son résultat.

```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` : un par ligne, avec l’`index` de la ligne, sa `reference`, ainsi que le `status` et le `body` que renverrait l’appel unitaire. Rapprochez chaque résultat de sa ligne de commande par `reference`.

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

Jusqu’à 500 lignes, mises en file d’attente sous forme d’une seule tâche. L’appel renvoie un `job_id` ; consultez la tâche jusqu’à ce que `status` vaille `done`, puis lisez `results`. Le même lot envoyé de nouveau alors que le premier est encore en file d’attente renvoie la première tâche avec `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`, ou `failed` avec un `message` lorsque la tâche n’a pas pu être traitée.
- Une tâche s’exécute une seule fois et n’est pas relancée. Un `rate_id` qui expire avant l’exécution de sa ligne renvoie `RATE_ID_INVALID` pour cette ligne ; envoyez la tâche de création peu après la fin de la tâche de devis.

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

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

**Vérification :** un lot renvoie un résultat par ligne ; une tâche asynchrone atteint `status` `done`.

## 13. Gestion des erreurs

| Situation | Statut HTTP | Code | Action de l’intégration |
|---|---|---|---|
| Un champ obligatoire est manquant ou mal formé | 400 | `VALIDATION_FAILED` | Corrigez le champ indiqué dans `message` et renvoyez la requête. |
| Le destinataire est hors de la zone de livraison (devis) | 200 | `OUT_OF_DELIVERY_AREA` dans `errors` | Proposez uniquement les tarifs `label_service`. |
| Le `rate_id` a expiré, est mal formé ou appartient à un autre compte | 400 | `RATE_ID_INVALID` | Demandez un nouveau devis et créez la commande avec son `rate_id`. Rien n’a été créé. |
| La commande d’étiquette a été créée mais son étiquette n’a pas été achetée | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Conservez l’`id` ; achetez l’étiquette avec `POST /api/v1/uniorder/{orderId}/label`. Ne créez jamais la commande une seconde fois. |
| L’étiquette est demandée avant d’avoir été achetée | 409 | `LABEL_PURCHASE_FAILED` | Achetez l’étiquette avec `POST /api/v1/uniorder/{orderId}/label`. |
| La commande est trop avancée pour être annulée | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Laissez la commande en l’état ; traitez le retour séparément. |
| La commande ne peut pas être annulée pour le moment | 409 | `ORDER_CANCEL_REFUSED` | Laissez la commande en l’état ; réessayez plus tard ou contactez l’entreprise. |
| Le transporteur n’a pas annulé l’étiquette | 409 | `LABEL_CANCEL_FAILED` | La commande reste inchangée ; relancez l’annulation plus tard. |
| La commande ou la tâche n’existe pas ou appartient à un autre compte | 404 | `ORDER_NOT_FOUND` | Vérifiez l’`id` conservé avec la commande web. |
| Un `Idempotency-Key` est réutilisé avec un corps différent | 409 | `IDEMPOTENCY_CONFLICT` | Utilisez une nouvelle clé pour une requête différente. |
| Le jeton est absent ou expiré, ou le compte n’est pas autorisé à passer des commandes | 401 | — | Reconnectez-vous ; vérifiez les autorisations du compte. |

## Liste de tests

Utilisez une `ref` de test telle que `WEB-10045` :

- [ ] Le devis renvoie un tarif `self_delivery` pour une adresse dans la zone.
- [ ] Avec `quote_labels`, le devis renvoie des tarifs `label_service`, chacun avec un `rate_id`.
- [ ] Commander avec un `rate_id` `self_delivery` renvoie `id` et `tracking_numbers`.
- [ ] Commander avec un `rate_id` `label_service` renvoie l’étiquette du service figurant dans le devis.
- [ ] Le même `Idempotency-Key` ne crée pas de seconde commande.
- [ ] Un `rate_id` de plus de 30 minutes renvoie `RATE_ID_INVALID`.
- [ ] L’étiquette de chaque commande se décode en un PDF imprimable.
- [ ] La commande, son étiquette et son suivi peuvent être consultés avec l’`id` obtenu à la création.
- [ ] L’annulation d’une commande de test renvoie `result: true` ; une nouvelle annulation renvoie `already_cancelled: true`.
- [ ] Après `LABEL_PURCHASE_FAILED`, `POST /api/v1/uniorder/{orderId}/label` achète l’étiquette pour la même commande.
- [ ] Un lot de deux lignes renvoie deux résultats avec leur `reference`.
