# Étiquettes transporteur

Acheter un envoi transporteur : lister les méthodes → tarifer → créer l’étiquette → télécharger le PDF → suivre → recevoir les événements → annuler (ou clôturer la journée).

Ce guide documente uniquement les opérations suivantes. Exécutez-les dans l’ordre.

Remplacez `YOUR_HOST`, `ACCESS_TOKEN` et `shipping_method` par les vôtres. Les id de méthode diffèrent par compte — ne les codez jamais en dur.

## 1. Authentification

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

```
Authorization: Bearer ACCESS_TOKEN
```

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

[Manuel REST](/api/documentation#/paths/v1-user-login/post) · [Manuel GraphQL](/api/graphql/documentation#/user/userLogin)

**Vérification :** la connexion renvoie `access_token`. Un appel ultérieur sans lui renvoie `401`.

## 2. Lister les méthodes d’expédition

**REST :** `POST /api/v1/labelservice/getShippingMethodList` — [Manuel 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}'
```

Chaque ligne a :

| Champ | Usage |
|---|---|
| `id` | `shipping_method` dans chaque appel suivant |
| `name` | Nom affiché |
| `unique_identifier` | Code stable |
| `options.signature_option` | Signature disponible |
| `options.insurance_option` | Assurance disponible |
| `options.multi_package` | Plus d’une pièce |

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

**Vérification :** la liste n’est pas vide. Vous avez choisi un `id` et vous savez si cette méthode autorise signature, assurance et plusieurs colis. Une liste vide signifie qu’aucune méthode n’est activée sur le compte.

## 3. Tarifer

Essai à blanc. On demande un prix au transporteur ; rien n’est réservé. Le corps a la même forme que pour créer. `shipping_method` est obligatoire.

**REST :** `POST /api/v1/labelservice/rate` — [Manuel 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": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

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

Pour plus d’une pièce, envoyez `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (id du carnet) ou `shipping_from_code` peut remplacer le bloc `sender_*`.

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

**Vérification :** `result` est true et vous avez un prix (et des jours de transit, si le transporteur les envoie). S’il n’y a pas de tarif, corrigez destination / colis / méthode **avant** de créer.

## 4. Créer l’étiquette

Cela réserve l’envoi auprès du transporteur.

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

Même corps que l’étape 3. Envoyez `Idempotency-Key`.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-label-001" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

**GraphQL :** `labelserviceSubmitOrder` ([Manuel GraphQL](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)).

```json
{
  "result": true,
  "id": 12345,
  "shipping_price": "12.50",
  "tracking_numbers": ["1Z999AA10123456784"],
  "external_id": "EXT_12345"
}
```

| Champ | Usage |
|---|---|
| `id` | Id de commande Superroute — téléchargement et annulation |
| `tracking_numbers` | Donnez-les à l’acheteur ; le suivi les accepte |
| `external_id` | Id d’envoi du transporteur |
| `shipping_price` | Montant facturé |

**Vérification :** `tracking_numbers` n’est pas vide. Stockez `id` et les numéros. Le même `Idempotency-Key` ne doit pas acheter une seconde étiquette.

## 5. Télécharger le PDF

**REST :** `POST /api/v1/labelservice/getShippingLabel` — [Manuel 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": "12345",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

`type` peut être `ORDER_ID`, `TRACKING_NUMBER` ou `REF`. `base64: 0` envoie un PDF. C’est l’**étiquette officielle du transporteur**. Le nombre de pièces est fixé par la réservation.

**GraphQL :** `labelserviceGetShippingLabel` ([Manuel GraphQL](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)).

**Vérification :** le PDF s’ouvre et montre le code-barres / numéro de suivi du transporteur de l’étape 4. Imprimez une copie de test, puis jetez-la — ne remettez pas une étiquette de test à un transporteur.

## 6. Suivre

Pas de jeton. Utilisez un numéro de `tracking_numbers` :

```bash
curl https://YOUR_HOST/api/v1/tracking/1Z999AA10123456784
```

[Manuel REST](/api/documentation#/operations/getPublicTracking) · [Manuel GraphQL](/api/graphql/documentation#/tracking/trackingPublic)

**GraphQL :**

```graphql
query {
  trackingPublic(trackingNumber: "1Z999AA10123456784") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

`is_third_party_tracking` est true lorsque les événements viennent du transporteur. Branchez-vous sur `tracking_event_status_id` / `tracking_event_key`, pas sur `description`. L’événement le plus récent est en premier dans `data`. Les événements précoces peuvent encore être « informations transmises » jusqu’au scan du colis par le transporteur. `500` est livré ; `proofs[]` peut alors inclure signature (`type` `1`) ou photo (`type` `2`).

**Vérification :** la recherche renvoie l’envoi que vous venez de créer. Un numéro inconnu est `result: false` / 404.

## 7. Configurer les notifications d’événements

| Réglage | Événement | Quand |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persister `id` et numéros de suivi transporteur |
| `tracking_event_webhook_url` | `tracking.event` | Scans transporteur, en cours de livraison, livré |
| `order_status_change_webhook_url` | `order.status_change` | Statut dans votre système |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Annulation refusée car le transporteur a déjà le colis |

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

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

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.example.com/hooks/superroute",
    "tracking_event_webhook_url": "https://erp.example.com/hooks/superroute",
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

Vérifiez **v2** sur le corps brut : `HMAC_SHA256(timestamp + "." + raw_body, secret)` contre `X-Webhook-Signature-V2`. Dédupliquez sur `X-Webhook-Event-Id`. Répondez **2xx en moins de 3 secondes**.

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

**Vérification :** un `submitOrder` de test produit `order.created` avec ces `tracking_numbers`. Un secret incorrect est `401` de votre récepteur.

## 8. Annuler

Uniquement tant que le transporteur l’autorise encore (en général avant l’enlèvement).

**REST :** `POST /api/v1/labelservice/cancelShippingLabel` — [Manuel 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" \
  -d '{"id":"12345","type":"ORDER_ID"}'
```

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

Un transporteur qui a déjà le colis refusera — c’est `order.cancel_failed`.

**Vérification :** une seconde annulation est sûre. Le suivi ne traite plus l’envoi comme actif.

## 9. Fin de journée (seulement si cette méthode l’exige)

Certains transporteurs ont besoin d’un manifeste quotidien.

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

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

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

Appelez-le une fois après la dernière étiquette de la journée d’expédition. Sautez cette étape lorsque la méthode de l’étape 2 n’a pas cette exigence.

**Vérification :** le corps de succès (ou la confirmation du transporteur) liste les étiquettes du jour. Exécutez-le d’abord sur une méthode de test.

## Liste de tests

Utilisez une destination que vous contrôlez et une méthode annulable :

- [ ] La liste des méthodes n’est pas vide ; vous avez capturé un `id`.
- [ ] Le tarif renvoie un prix pour cette méthode et cette destination.
- [ ] Submit renvoie `tracking_numbers` ; le même `Idempotency-Key` n’achète pas une seconde étiquette.
- [ ] Le PDF de l’étiquette s’ouvre et montre le numéro de suivi du transporteur.
- [ ] Le suivi public trouve l’envoi par ce numéro.
- [ ] `order.created` arrive ; la signature v2 vérifie.
- [ ] L’annulation réussit, **ou** vous avez confirmé que cette méthode ne peut pas être annulée après réservation.
- [ ] Si la méthode a besoin de la fin de journée, un essai se termine sans erreur.
