DeliveryCloud API nShift : intégration, authentification et création d’envois

Tutoriel pratique pour intégrer l’API DeliveryCloud (nShift) à PrestaShop : authentification OAuth2, normalisation des payloads, idempotence, retries, sécurité et environnements de test.

Personne devant un ordinateur, entourée d'icônes technologiques et de graphiques numériques.

Table des matières :

  1. DeliveryCloud (nShift) côté e-commerce : ce que vous intégrez réellement
  2. Pré-requis d’intégration PrestaShop : versions, dépendances, points de friction
  3. Authentification DeliveryCloud : OAuth2, cycle de vie du token et stockage
  4. Architecture module : séparation des responsabilités (et pourquoi le “tout dans le controller” vous tue)
  5. Création d’envoi : mapping PrestaShop → DeliveryCloud, idempotence et persistance
  6. Appels API : timeouts, retries, journalisation, et gestion des erreurs exploitables
  7. Sécurité et conformité : secrets, données personnelles, et webhooks de tracking
  8. Tests, environnement sandbox et observabilité : éviter le “ça marche chez moi”

DeliveryCloud (nShift) côté e-commerce : ce que vous intégrez réellement

DeliveryCloud (nShift) n’est pas un « plugin transporteur » au sens PrestaShop : c’est une API d’orchestration d’expéditions. Concrètement, vous externalisez (a) la sélection du service transporteur, (b) la génération d’étiquette, (c) la création d’envoi et le suivi, et souvent (d) la gestion d’options type point relais, assurance, notifications. Côté PrestaShop, cela implique une vraie couche d’intégration : mapping des données commande/adresse/colis, persistance des identifiants externes, et gestion d’erreurs réseau.

Le point clé pour éviter les impasses : distinguer le “shipping rate” (tarification / choix de mode de livraison) de la “shipment creation” (création de l’envoi une fois la commande payée). Dans la majorité des projets PrestaShop, la création d’envoi se déclenche sur un évènement métier (paiement accepté, préparation, ou passage d’un état commande spécifique). Tenter de « créer l’envoi » dès le checkout est une erreur fréquente : vous multipliez les annulations, les doublons et vous exposez des secrets API à des flux front.

Enfin, DeliveryCloud étant multi-transporteurs et multi-pays, les champs requis varient énormément (douanes, EORI, incoterms, formats de téléphone, codes de service, dimensions obligatoires, etc.). Le cœur de PrestaShop n’offre pas d’abstraction unifiée pour ça : vous devrez assumer une normalisation stricte, sinon l’API refusera vos envois ou générera des étiquettes incohérentes.

Quelques implications très concrètes (souvent sous-estimées) côté e-commerce en France / UE :

  • Relais / points de retrait : très utilisés en France (Mondial Relay, Relais Colis, Pickup/Chronopost, etc.). L’adresse “destinataire” peut devenir l’adresse du point relais, tandis que le client reste le “consumer/recipient” pour contact. Votre mapping doit gérer ce double niveau sans écraser les données.
  • Spécificités DOM-TOM : selon transporteurs, la “France” n’implique pas les mêmes services ni les mêmes règles. Un code postal 97xxx peut basculer sur des services dédiés, des délais, et parfois des documents supplémentaires.
  • Transfrontalier : Suisse, Royaume-Uni, Norvège (et, selon votre activité, export hors UE) déclenchent rapidement des exigences douanières (HS code, valeur, origine, incoterm, EORI, etc.). PrestaShop ne vous fournit pas tout “prêt à l’envoi”.

Sources utiles (à consulter pour les schémas exacts d’API et les endpoints à jour) : documentation nShift et portail développeur DeliveryCloud (selon votre tenant / région).

Pré-requis d’intégration PrestaShop : versions, dépendances, points de friction

Les exemples ci-dessous sont pensés pour PrestaShop 8.1+ / 9.1 (Symfony présent) et PHP 8.2 (compatible également avec PHP 8.1+ sur PrestaShop 9.1). Si vous êtes en 1.7, vous pouvez intégrer, mais la structure (services Symfony, commandes Console, etc.) n’est pas la même et vous retombez vite dans une dette technique. Pour le socle runtime, vérifiez d’abord votre matrice versionnelle côté boutique : l’article interne sur PrestaShop 9.1 et la compatibilité PHP vous évitera des surprises de modules incompatibles ou de dépendances Composer cassées : https://www.expertise-prestashop.fr/2026/03/24/prestashop-9-1-compatibilite-php-8-1-8-5-cli-et-nouveautes-developpeurs/

Côté dépendances HTTP, deux approches réalistes :

  • Symfony HttpClient (déjà dans l’écosystème Symfony, bonne intégration, mocks propres en test).
  • Guzzle si vous avez déjà une stack modules qui standardise dessus.

Dans les deux cas, imposez des timeouts, des retries contrôlés, et un logging exploitable. Le “fire-and-forget” via file_get_contents() est un anti-pattern : vous perdez la traçabilité et vous explosez les temps de traitement back-office.

Enfin, côté persistance, partez du principe que vous aurez besoin d’une table dédiée (ex. ps_nshift_shipment) plutôt que de tout stocker dans ps_orders ou ps_order_carrier. PrestaShop n’a pas de modèle natif pour stocker un identifiant d’envoi externe, un état d’étiquette, des URLs de label, des numéros de tracking multiples, etc.

Pour cadrer dès le départ ce que vous devez réellement stocker, voici une checklist utile (à adapter) :

  • id_order (liaison PrestaShop)
  • shipment_id externe (nShift/DeliveryCloud)
  • carrier_reference / service code (utile quand vous changez de transporteur à iso-fonction)
  • status (created / label_ready / cancelled / error / pending…)
  • tracking_number (et éventuellement un JSON si multi-colis)
  • label_url ou label_blob_path (si vous proxy/stockez)
  • correlation_id (pour recoller logs ↔ tentatives)
  • last_error_code, last_error_message (diagnostic BO)
  • created_at, updated_at

Exemple minimaliste de DDL (MySQL/MariaDB) pour matérialiser l’idée :

CREATE TABLE ps_nshift_shipment (
  id_nshift_shipment INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  id_order INT UNSIGNED NOT NULL,
  shipment_id VARCHAR(64) NOT NULL,
  service_code VARCHAR(64) NULL,
  status VARCHAR(32) NOT NULL DEFAULT 'created',
  tracking_number VARCHAR(64) NULL,
  label_url TEXT NULL,
  correlation_id CHAR(36) NULL,
  last_error_code VARCHAR(32) NULL,
  last_error_message VARCHAR(255) NULL,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL,
  UNIQUE KEY uniq_order_shipment (id_order),
  UNIQUE KEY uniq_shipment_id (shipment_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Important : la contrainte d’unicité ne dépend pas seulement de id_order si vous gérez multi-colis / multi-envois par commande (split shipment). Dans ce cas, introduisez shipment_key (ex. order_id + parcel_index) et ajustez les index.

Authentification DeliveryCloud : OAuth2, cycle de vie du token et stockage

Dans la majorité des intégrations nShift DeliveryCloud, l’authentification se fait via un schéma OAuth 2.0 (souvent client credentials). Le RFC de référence rappelle le périmètre exact : « The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service… » (RFC 6749, IETF) : https://datatracker.ietf.org/doc/html/rfc6749

Implémentation côté module : vous devez gérer l’acquisition du token, sa mise en cache, son rafraîchissement et l’horloge (clock skew). Un token “valide 3600s” n’est jamais fiable à la milliseconde : appliquez une marge de sécurité (ex. renouveler à expires_in - 60s). En PrestaShop 8/9, évitez Configuration::updateValue() pour stocker le token en clair : au minimum, utilisez un cache applicatif (Symfony Cache, APCu si dispo) et retombez sur une acquisition si cache manquant.

Deux points pratiques qui font gagner du temps en production :

  • Tolérance aux erreurs d’auth : si un appel API renvoie 401, ne bouclez pas en retry “classique”. Invalidez le token cache, refaites une acquisition, puis rejouez l’appel une fois.
  • Différencier token et secrets : le token (éphémère) peut vivre en cache. Les secrets (clientid/clientsecret) doivent vivre dans un stockage plus strict (env, vault, etc.).

Pour les secrets (clientid/clientsecret), ne vous racontez pas d’histoire : PrestaShop ne chiffre pas nativement les valeurs en base. Les stocker en ps_configuration est acceptable uniquement si vous avez déjà une hygiène de serveur solide (droits DB, backups chiffrés, accès restreints). Sinon : variables d’environnement, fichier de conf hors webroot, ou secret manager (Vault/KMS). Pour cadrer les risques, appuyez-vous sur les pratiques listées dans l’article interne sécurité : https://www.expertise-prestashop.fr/2026/04/20/securite-prestashop-2026-risques-majeurs-et-protections-professionnelles/

Exemple (simplifié) avec Symfony HttpClient, en supposant un endpoint token fourni par nShift (à remplacer par votre URL réelle documentée) :

use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface;

final class NshiftAuth
{
    public function __construct(
        private HttpClientInterface $http,
        private LoggerInterface $logger,
        private string $clientId,
        private string $clientSecret,
        private string $tokenUrl,
        private \Symfony\Component\Cache\Adapter\AdapterInterface $cache,
    ) {}

    public function getAccessToken(): string
    {
        $cacheKey = 'nshift.oauth.access_token';

        return $this->cache->get($cacheKey, function ($item) {
            $item->expiresAfter(300); // valeur par défaut si l'API ne renvoie rien

            $resp = $this->http->request('POST', $this->tokenUrl, [
                'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
                'body' => http_build_query([
                    'grant_type' => 'client_credentials',
                    'client_id' => $this->clientId,
                    'client_secret' => $this->clientSecret,
                ]),
                'timeout' => 10,
            ]);

            $data = $resp->toArray(false);
            if (empty($data['access_token'])) {
                $this->logger->error('nShift token response invalid', ['data' => $data]);
                throw new \RuntimeException('Impossible de récupérer le token nShift.');
            }

            $expiresIn = (int)($data['expires_in'] ?? 300);
            $item->expiresAfter(max(60, $expiresIn - 60)); // marge de 60s

            return (string)$data['access_token'];
        });
    }
}

Architecture module : séparation des responsabilités (et pourquoi le “tout dans le controller” vous tue)

Une intégration propre DeliveryCloud se pense en couches : Client HTTP, Auth, Builders de payload, Use-cases (createShipment, cancelShipment, getLabel, track), et Adaptateurs PrestaShop (hooks, OrderState, BO actions). Si vous mettez tout dans un contrôleur Admin, vous perdez testabilité et vous finissez avec un code impossible à maintenir dès qu’un transporteur demande un champ additionnel.

Sur PrestaShop 8/9, exploitez le conteneur Symfony : services déclarés, injection de dépendances, configuration par environnement. Cela permet aussi d’écrire des commandes CLI (ex. bin/console nshift:sync:tracking) qui tournent en cron sans dépendre du back-office. Si vous devez traiter 500 expéditions/jour, la CLI + queue (ou à défaut cron) est souvent plus robuste que l’exécution synchrone lors d’un clic BO.

Côté déclenchement, évitez de surcharger le checkout tant que vous n’avez pas verrouillé votre flux métier. Un pattern réaliste :

1) L’admin prépare la commande (ou état “Paiement accepté”).
2) Votre module crée l’envoi DeliveryCloud.
3) Vous stockez shipment_id, label_url/label_pdf, tracking_number.
4) Vous poussez le tracking dans OrderCarrier et/ou un bloc BO.

Ce découplage vous permet aussi de rejouer une création d’envoi en cas d’échec réseau sans regénérer des colis incohérents.

Mini-scenario (typique BO) : un préparateur clique sur “Créer l’étiquette”, la requête met 12 secondes, l’UI n’affiche rien, il reclique. Sans séparation claire (use-case + persistance + idempotence), vous pouvez créer deux expéditions facturées. Avec une table ps_nshift_shipment et un statut pending, vous pouvez : (a) bloquer le deuxième clic, (b) afficher “Création en cours”, (c) reprendre automatiquement si timeout.

Création d’envoi : mapping PrestaShop → DeliveryCloud, idempotence et persistance

La création d’envoi est le point où 90% des erreurs apparaissent : adresses mal normalisées, téléphone vide, poids à zéro, pays non géré, champs douane manquants, etc. PrestaShop laisse passer beaucoup de données “à peu près” (ex. téléphones avec espaces, champs address2 utilisés comme fourre-tout). L’API transport, elle, est souvent stricte. Vous devez donc implémenter une normalisation : trimming, validation E.164 pour téléphone si requis, fallback email, et surtout séparation claire street / house number si votre transporteur le demande.

Sur un shop FR/BE/CH, quelques règles simples évitent beaucoup de 400 :

  • Téléphone : si vous expédiez en France, préférez stocker/émettre en +33… quand c’est requis (E.164). Dans le doute, normalisez et laissez null plutôt qu’un champ manifestement invalide (ex. 0000000000) : certains transporteurs refusent les valeurs “bidon”.
  • Code postal : attention aux espaces (ex. UK) et aux codes alphanumériques. Ne forcez pas un int.
  • Nom/prénom : certains services imposent une longueur max ; gardez une troncature maîtrisée (et loggez quand vous tronquez).

Deuxième point non négociable : l’idempotence. L’utilisateur BO qui clique deux fois, un timeout HTTP, un retry mal géré, et vous créez deux envois facturés. Si DeliveryCloud ne fournit pas de clé d’idempotence explicite, vous devez simuler :

  • générer un identifiant stable côté PrestaShop (ex. order_reference + carrier_id + attempt),
  • l’injecter dans un champ de référence supporté par l’API (si disponible),
  • et persister la correspondance dans votre table ps_nshift_shipment avec une contrainte d’unicité.

Troisième point : le “colisage” (parcels). PrestaShop ne gère pas nativement des colis multiples par commande. Si vous avez du multi-colis, vous devez construire votre propre logique (poids total → répartition, dimensions par SKU si vous les stockez, etc.). À défaut, vous expédiez “1 colis” avec poids total, ce qui fonctionne pour certains transporteurs mais casse dès que des dimensions sont obligatoires ou que le réseau relais impose des limites.

Exemple de structure de données (conceptuelle) pour un builder. Le schéma exact dépend du tenant nShift / transporteurs activés ; référez-vous à la doc officielle nShift pour les champs attendus.

final class ShipmentPayloadBuilder
{
    public function buildFromOrder(\Order $order, \Address $delivery, array $parcels): array
    {
        return [
            'references' => [
                'order_reference' => $order->reference,
                'order_id' => (string)$order->id,
            ],
            'recipient' => [
                'name' => trim($delivery->firstname.' '.$delivery->lastname),
                'company' => $delivery->company ?: null,
                'email' => $delivery->email ?: null,
                'phone' => $this->normalizePhone($delivery->phone ?: $delivery->phone_mobile),
                'address' => [
                    'line1' => $delivery->address1,
                    'line2' => $delivery->address2 ?: null,
                    'postal_code' => $delivery->postcode,
                    'city' => $delivery->city,
                    'country_iso' => \Country::getIsoById((int)$delivery->id_country),
                ],
            ],
            'parcels' => array_map(fn($p) => [
                'weight_kg' => $p['weight_kg'],
                'length_cm' => $p['length_cm'] ?? null,
                'width_cm'  => $p['width_cm'] ?? null,
                'height_cm' => $p['height_cm'] ?? null,
            ], $parcels),
            // 'service' => ['code' => '...'],
            // 'pickup_point' => [...],
        ];
    }

    private function normalizePhone(?string $raw): ?string
    {
        $raw = $raw ? preg_replace('/\s+/', '', $raw) : null;
        return $raw ?: null;
    }
}

Si vous faites de l’international hors UE, prévoyez dès maintenant un endroit où enrichir le payload avec des lignes douane (description, quantité, valeur, poids, origine, HS code). Même si votre MVP n’expédie qu’en France, c’est typiquement un “petit ajout” qui devient une refonte si l’architecture n’a pas prévu un objet/structure extensible.

Appels API : timeouts, retries, journalisation, et gestion des erreurs exploitables

Votre module doit traiter DeliveryCloud comme un service externe instable par nature : latence, erreurs 5xx, rate limit, timeouts. Par défaut, ne laissez jamais PHP bloquer 60 secondes sur une requête : imposez timeout et max_duration (Symfony) ou connect_timeout/timeout (Guzzle). Si la création d’envoi se fait en BO synchrone, vous devez conserver une UX acceptable (et éviter les doubles clics) : spinner + verrouillage côté UI, et surtout un statut “pending” en base.

Sur les retries : appliquez un retry uniquement sur les erreurs réseau/5xx et jamais sur les erreurs fonctionnelles (4xx validation). Et faites-le avec backoff (idéalement exponentiel) + jitter. La raison est simple : un retry mal conçu peut aggraver la charge d’un service déjà dégradé, et transformer une lenteur en incident.

Une grille de décision pragmatique (facile à maintenir) :

Type d’erreur Exemple Retry ? Action
Validation fonctionnelle 400 / payload invalide Non Marquer error, afficher message BO exploitable
Auth 401 1 tentative max Invalider token cache, ré-auth, rejouer une fois
Rate limit 429 Oui (limité) Backoff + planifier via cron/queue si possible
Erreur serveur 500/502/503 Oui (limité) Backoff + idempotence obligatoire
Timeout réseau client timeout Oui (limité) Ne pas “recliquer” en BO : planifier un retry

Côté logging, sortez des logs “console” inutiles : loggez un correlation_id (UUID) par tentative, l’ID commande, le code HTTP, et un extrait contrôlé de la réponse (sans données sensibles). Pour la partie PHP/erreurs runtime, alignez-vous sur une config propre dev/prod (display_errors off en prod, logs centralisés, etc.) : https://www.expertise-prestashop.fr/2026/04/02/gestion-derreur-php-bonnes-pratiques-et-configuration-developpement-production/

  • NshiftApiClient lève des exceptions typées (NshiftAuthException, NshiftValidationException, NshiftTransportException).
  • Le use-case CreateShipmentHandler attrape, persiste un état, et renvoie un message BO clair.
  • En cas de timeout, vous ne “réessayez” pas à l’aveugle en synchrone : vous planifiez un retry via cron/queue, en vérifiant l’idempotence.

Astuce BO qui change la vie en exploitation : quand vous affichez une erreur, exposez un identifiant de diagnostic (le correlation_id). Même sans stack ELK, ça permet de retrouver l’événement exact dans les logs serveur.

Sécurité et conformité : secrets, données personnelles, et webhooks de tracking

L’intégration transport manipule des données personnelles (adresse, téléphone, email). Ne répliquez pas inutilement ces données en base dans votre table d’envois : stockez des références (order_id) et les identifiants externes. Si vous devez stocker un payload de debug, faites-le (a) chiffré, (b) avec TTL de purge, (c) accessible uniquement aux rôles BO nécessaires. Pour le cadre global des risques (RCE modules, fuites de conf, permissions), l’article sécurité interne donne un panorama concret : https://www.expertise-prestashop.fr/2026/04/20/securite-prestashop-2026-risques-majeurs-et-protections-professionnelles/

Pour les secrets, la règle est simple : pas de secret en front, pas de secret dans Git, pas de secret dans des endpoints de module non protégés. Si vous exposez une action BO “Imprimer l’étiquette”, elle doit vérifier le token admin / permissions et idéalement servir le fichier depuis le serveur (proxy) plutôt que d’exposer une URL directe signée longue durée.

Si vous activez des webhooks (tracking, événements transporteur), ne les traitez pas comme un simple controller qui met à jour la commande. Implémentez :

  • vérification de signature (HMAC ou équivalent selon nShift),
  • anti-rejeu (timestamp + nonce),
  • validation de schéma, et
  • journalisation des événements.

Référence transversale utile : OWASP API Security Top 10 (contrôles de base sur auth, validation, logging) : https://owasp.org/www-project-api-security/

Point conformité (souvent oublié) : si vous transmettez des données à un prestataire transport/orchestration, il vous faut généralement cadrer cela via votre documentation RGPD interne (registre de traitements, DPA/contrat de sous-traitance, durées de conservation). Côté module, ça se traduit surtout par : minimisation (ne stocker que ce qui est nécessaire) et purge (ne pas conserver indéfiniment des payloads/logs contenant des données de livraison).

Tests, environnement sandbox et observabilité : éviter le “ça marche chez moi”

Sans environnement sandbox (ou tenant de test) et sans jeu de données contrôlé, vous ne testez pas une intégration transport : vous la “tentez”. Mettez en place un mode NSHIFT_ENV=test dans la config du module, avec endpoints distincts, et surtout des transporteurs/services de test documentés par nShift. En local, mockez l’HTTP (Symfony MockResponse) pour couvrir : token expiré, 400 validation, 429 rate limit, 500, timeout.

Écrivez au minimum des tests de contrat sur votre builder de payload : à partir d’une commande PrestaShop figée (fixtures), vous devez produire un JSON stable (ou quasi) et vérifier la présence/format de champs critiques. Exemple : pays ISO, postcode non vide, poids > 0, email valide si requis. Ce genre de tests évite les régressions quand un intégrateur modifie un champ adresse dans le thème ou qu’un module tiers change la structure des données.

Enfin, observez vos flux : compteur d’échecs de création d’envoi, temps moyen de réponse API, volume de retries, nombre de doublons évités par idempotence. Si vous avez déjà une stack (Prometheus/Grafana, ELK, OpenTelemetry), branchez votre client HTTP. Sinon, commencez simplement par des logs structurés et un export CSV BO. L’intégration transport devient vite un point de friction opérationnel ; sans métriques, vous ne distinguez pas un bug de mapping d’une dégradation côté API.

En pratique, une intégration DeliveryCloud API nShift fiable dans PrestaShop tient sur trois piliers : auth solide (tokens + secrets), payload strict (normalisation + idempotence), résilience (timeouts/retries/monitoring). Le reste (UI BO, impression étiquettes, tracking) n’est “que” l’interface d’exploitation — mais c’est là que vos utilisateurs jugeront si l’intégration est réellement utilisable au quotidien.


À lire aussi