# Enlèvement et livraison

L’enlèvement et le dernier kilomètre en flotte propre utilisent les mêmes API. Créer une commande d’enlèvement ou de livraison → imprimer l’étiquette locale → suivre → s’abonner aux notifications d’événements → annuler une commande de test.

`type` sélectionne l’arrêt : `D` livraison, `P` enlèvement. Exécutez les étapes dans l’ordre. Le tarif est facultatif et n’est pas exigé avant la création.

Remplacez `YOUR_HOST` et `ACCESS_TOKEN` par les valeurs de votre environnement.

## 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"}'
```

Placez l’`access_token` renvoyé dans l’en-tête de la requête :

```
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 ce jeton renvoie `401`.

## 2. Tarifer (facultatif)

Cette étape est facultative. Elle renvoie uniquement un tarif ; aucune commande n’est créée. La création n’exige pas de tarif préalable. Définissez `type` à `D` (livraison) ou `P` (enlèvement).

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

```bash
curl -X POST https://YOUR_HOST/api/v1/orders/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_postcode": "H9S2H9",
    "from_country": "CA",
    "to_postcode": "H4B2T5",
    "to_country": "CA",
    "packages": [{
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "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,
  "shipping_price": "6.99",
  "currency": "CAD",
  "price_details": {
    "shipping_fee": "6.99",
    "tax_details": [{ "tax_name": "HST", "tax_rate": "13.00", "tax": "0.91" }]
  }
}
```

**GraphQL** (scalaire JSON — pas de selection set) (`ordersRate` ([Manuel GraphQL](/api/graphql/documentation#/orders/ordersRate))):

```graphql
mutation {
  ordersRate(
    type: "D"
    from_postcode: "H9S2H9"
    from_country: "CA"
    to_postcode: "H4B2T5"
    to_country: "CA"
    packages: [{ weight: 1, weight_unit: 2, length: 30, width: 20, height: 10, dimension_unit: 2 }]
  )
}
```

**Vérification :** si vous exécutez cet appel, `result` est true et `shipping_price` est un nombre. Répétez avec `type` `P` pour tarifer un enlèvement. L’absence de prix signifie que le code postal n’est pas dans une zone active. La création ne dépend pas de cette étape.

## 3. Créer la commande

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

La requête doit inclure `Idempotency-Key` afin qu’une relance ne puisse pas créer une seconde commande.

### Livraison (`type` `D`)

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-local-001" \
  -d '{
    "type": "D",
    "need_pick_up": 0,
    "ref": "DEV-LOCAL-001",
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "delivery_instruction": "Leave at the side door"
  }'
```

### Enlèvement (`type` `P`)

Le même point de terminaison. L’adresse est le point d’enlèvement.

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-pickup-001" \
  -d '{
    "type": "P",
    "need_pick_up": 1,
    "ref": "DEV-PICKUP-001",
    "name": "Jane Sender",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-P-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "pickup_instruction": "Ring the side bell"
  }'
```

| Champ | Signification |
|---|---|
| `type` | `D` livraison ou `P` enlèvement |
| `need_pick_up` | `0` — déjà à l’entrepôt. `1` — un chauffeur doit enlever le colis |
| `ref` | Référence externe pour retrouver et rapprocher |
| `name` / address | Livraison : destinataire. Enlèvement : point d’enlèvement |
| `auto_deduplication` | `1` refuse un second colis avec la même `ref` de colis |

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

```json
{ "result": true, "id": 12345, "tracking_number": "SR123456789012", "orders_status_id": 2 }
```

`orders_status_id` `2` est Nouveau. Stockez `id`, `tracking_number` et `ref`.

**Vérification :** renvoyez le **même** corps avec le même `Idempotency-Key`. Vous devez obtenir le même `id` et ne devez pas créer une seconde commande.

La commande n’est **pas** créée lorsque :

| `code` | Que faire |
|---|---|
| `INSUFFICIENT_BALANCE` | `insufficient_balance` contient required / available / shortfall. Rechargez, puis réessayez. |
| `OUT_OF_DELIVERY_AREA` | L’adresse est hors de la zone de service et l’entreprise supprime ces commandes. Soumettez une adresse dans la zone de service. |
| `IDEMPOTENCY_CONFLICT` | La même clé d’idempotence a été réutilisée avec un corps de requête différent. Émettez une nouvelle clé. |

Une commande hors zone conservée peut quand même renvoyer `result: true` avec `shipping_price: null` et un `warning`. Lisez ce champ.

## 4. Consulter la commande

```bash
curl "https://YOUR_HOST/api/v1/orders/list?page=1&per_page=50" \
  -H "Authorization: Bearer ACCESS_TOKEN"

curl https://YOUR_HOST/api/v1/orders/12345 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**REST :** `GET /api/v1/orders/list` — toutes les commandes du compte, les plus récentes d’abord, chacune avec ses colis et leurs articles. Envoyez `page` et `per_page` ensemble pour paginer (`per_page` au maximum 1000) ; sans eux vous recevez les 1000 commandes les plus récentes et un indicateur `truncated`. [Manuel REST](/api/documentation#/paths/v1-orders-list/get)

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

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

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

**Vérification :** la commande appartient au compte authentifié. `ref` correspond à la valeur créée. `tracking_number` correspond à l’étape 3.

## 5. Imprimer l’étiquette locale

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

```bash
curl -X POST https://YOUR_HOST/api/v1/shipping/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "SR123456789012",
    "type": "TRACKING_NUMBER",
    "base64": 1,
    "hide_sender_address": 0,
    "hide_receiver_address": 0
  }'
```

`type` : `TRACKING_NUMBER`, `ORDER_ID` ou `REF`. `base64: 0` (défaut) envoie un PDF. Avec `base64: 1`, tout le corps de la réponse est une chaîne JSON de premier niveau contenant le PDF en base64, et non un objet avec un champ `pdf_data`. Appelez plutôt `POST /api/v2/shipping/getShippingLabel` si vous souhaitez recevoir l'étiquette dans un objet JSON classique.

**GraphQL :** `shippingGetShippingLabel` ([Manuel GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([Manuel GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) renvoie toujours du JSON (`pdf_data`).

**Vérification :** le PDF s’ouvre. Une étiquette de livraison affiche le destinataire ; une étiquette d’enlèvement affiche l’adresse d’enlèvement. Un champ d’adresse masqué est rendu vide sur l’étiquette.

## 6. Suivre

Point de terminaison public de suivi ; aucun jeton d’accès n’est requis.

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

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

La même URL accepte votre `ref` lorsqu’elle a été enregistrée comme numéro externe.

**GraphQL** (typé — besoin d’un selection set) :

```graphql
query {
  trackingPublic(trackingNumber: "SR123456789012") {
    result
    deliveried
    returntosender
    rejectedbyrecipient
    postcode
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

Branchez-vous sur `tracking_event_status_id`, pas sur `description` (cette chaîne suit `Accept-Language`) :

| `tracking_event_status_id` | Côté | Signification |
|---|---|---|
| `100` | les deux | Commande reçue |
| `300` / `301` | livraison | En entrepôt |
| `450` | livraison | En cours de livraison |
| `500` | livraison | Livré |
| `501` | livraison | Livraison échouée, nouveau plan nécessaire |
| `460` | enlèvement | En cours d’enlèvement |
| `510` | enlèvement | Enlevé |
| `512` | enlèvement | Enlèvement échoué, réessayer plus tard |
| `513` | enlèvement | Problème d’enlèvement |

`data` est du plus récent au plus ancien. Traitez la première ligne comme l’état actuel. `deliveried: true` après `500`.

Sur `500` ou `510`, `proofs[]` peut porter `type` `1` (signature) ou `2` (photo), plus `file_id` et `signed_url`. Une photo téléversée après cet événement n’est pas dans ce payload — abonnez-vous à `pod.files_updated` à l’étape 7.

**Vérification :** juste après la création, l’événement le plus récent est `100` et `deliveried` est false. Un numéro inconnu est `result: false` / 404 — affichez introuvable, n’inventez pas d’événements de suivi.

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

Configurez les URL de rappel exigées par ce parcours :

| Réglage | Événement | Quand |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persister `id` et `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Statut visible par le client |
| `tracking_event_webhook_url` | `tracking.event` | Chronologie d’enlèvement ou de livraison |
| `pod_files_webhook_url` | `pod.files_updated` | Photo/signature après enlèvement ou livraison |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Une annulation que vous avez envoyée a été refusée |

**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",
    "order_status_change_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 :** créez une commande de test et voyez `order.created` avec le même `id` / `tracking_number`. Une signature invalide doit être rejetée par le récepteur avec `401`. Une seconde livraison du même `X-Webhook-Event-Id` ne doit pas être traitée deux fois.

## 8. Annuler une commande de test

**REST :** `POST /api/v1/orders/cancel` — [Manuel REST](/api/documentation#/paths/v1-orders-cancel/post) — exactement un parmi `order_id`, `tracking_number`, `external_tracking_number`.

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

Déjà annulée : `200` avec `already_cancelled: true`. **GraphQL :** `ordersCancel` ([Manuel GraphQL](/api/graphql/documentation#/orders/ordersCancel)).

**Vérification :** le suivi public ne traite plus l’envoi comme actif. Si l’annulation est refusée (`409`, par exemple `ORDER_ALREADY_IN_DELIVERY`), `order.cancel_failed` se déclenche.

## 9. Création par lot (optionnel)

- `POST /api/v1/client/batchOrderCreate` — [Manuel REST](/api/documentation#/paths/v1-client-batchOrderCreate/post) — attend que chaque ligne ait été traitée.
- `POST /api/v1/client/batchOrderCreateAsync` — [Manuel REST](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — renvoie immédiatement un identifiant de tâche ; interrogez `GET /api/v1/client/async/{id}` — [Manuel REST](/api/documentation#/paths/v1-client-async-id/get) ou prenez `order.create_async`.

Mêmes champs que l’étape 3, sous forme de tableau de commandes. Chaque ligne peut être `type` `D` ou `P`. **GraphQL :** `clientBatchOrderCreate` ([Manuel GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreate)) / `clientBatchOrderCreateAsync` ([Manuel GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)).

**Vérification :** chaque ligne a son propre `result`. Utilisez le point de terminaison asynchrone au-delà d’environ 100 lignes.

## Liste de tests

Utilisez un `ref` de test comme `DEV-LOCAL-001` / `DEV-PICKUP-001` :

- [ ] (Facultatif) Le tarif renvoie un prix pour un code postal dans la zone avec `type` `D`.
- [ ] (Facultatif) Le tarif renvoie un prix pour un code postal dans la zone avec `type` `P`.
- [ ] La création d’une livraison renvoie `id` + `tracking_number` ; le même `Idempotency-Key` ne crée pas une seconde commande.
- [ ] La création d’un enlèvement renvoie `id` + `tracking_number` ; `need_pick_up` est `1`.
- [ ] Liste / détail affiche la commande sous ce compte.
- [ ] Le PDF de l’étiquette locale s’ouvre et montre le destinataire ou l’adresse d’enlèvement.
- [ ] Le suivi public renvoie la chronologie sans jeton ; l’événement le plus récent est `100`.
- [ ] `order.created` arrive ; la signature v2 vérifie.
- [ ] L’annulation renvoie `result: true` (ou `already_cancelled: true` à la relance).
