API d'intégration · version 1

Documentation de l'API d'intégration La Voie Express

Ce guide décrit tout ce que votre équipe technique doit savoir pour créer des expéditions, suivre leur acheminement et demander un retour — sans passer par notre portail web. Chaque endpoint est décrit par ce qu'il attend, ce qu'il renvoie et ce qu'il déclenche réellement.

Centre logistique La Voie Express
L'infrastructure logistique, accessible par API.

Principes de l'API

Cinq règles gouvernent toute l'API. Les comprendre en amont évite l'essentiel des allers-retours pendant l'intégration.

Suivi et notification Les statuts sont conservés dans un registre que vous pouvez interroger à tout moment. Si une URL de callback est configurée pour votre compte, LVE peut également lui envoyer le statut courant dès qu'un traitement interne est terminé.
Tout est JSON Requêtes et réponses en application/json, encodage UTF-8. Les dates sont au format ISO 8601 sans fuseau, par exemple 2026-07-20T10:00:00.
Enveloppe unique Toute réponse a la même forme. Succès : { "success": true, "data": … }. Erreur : { "success": false, "message": "…" }. Testez toujours success avant de lire data.
Créations rejouables Renvoyer exactement la même création ne crée pas de doublon : vous récupérez l'expédition déjà enregistrée. Un timeout réseau n'est donc jamais une raison de ne pas réessayer.
Cloisonnement Vous ne voyez que vos propres expéditions. Une référence appartenant à un autre client répond 404, exactement comme une référence inexistante.
Adresse de l'API et identifiants. L'URL de base, votre codeClient et votre secret vous sont transmis par LVE à l'ouverture du compte. Le secret n'est affiché qu'une seule fois : conservez-le dans votre coffre à secrets, jamais dans le code source.

Parcours d'intégration

L'ordre ci-dessous est celui d'une intégration réelle : chaque étape dépend de la précédente.

  1. Obtenir un jeton

    Votre codeClient et votre secret sont échangés contre un jeton d'accès valable 30 minutes. Toutes les autres requêtes le présentent en en-tête. Prévoyez de le renouveler automatiquement, pas de le stocker durablement.

  2. Créer l'expédition

    Une expédition par destination, ou un lot groupé quand plusieurs destinations partent d'un même point d'enlèvement. Vous recevez en retour une référence LVE et un numéro de suivi : enregistrez-les dans votre système, ce sont vos seules clés pour la suite.

  3. Suivre l'acheminement

    Interrogez le registre de statuts à intervalle régulier. Utilisez le paramètre since pour ne récupérer que les évènements nouveaux depuis votre dernier appel : c'est plus léger pour vous comme pour nous.

  4. Traiter les cas particuliers

    Un refus du destinataire, une annulation ou un retour demandé par vos soins produisent des statuts spécifiques. Votre système doit savoir les interpréter — le diagramme ci-dessous montre tous les chemins possibles.

Cycle de vie d'une expédition

Une expédition avance de point de contrôle en point de contrôle. Deux sorties existent : l'annulation avant mise en circuit, et le retour — que celui-ci vienne d'un refus du destinataire ou d'une demande de votre part.

CREEE RAMASSE EN_AGENCE_TRI EN_LIVRAISON LIVRE ANNULEE annulation avant mise en circuit refus du destinataire REFUSE RETOUR_EN_COURS RETOUR_TERMINEE POST /demandes/{n}/retour
parcours nominal sortie de parcours action de votre côté
Les neuf statuts et les transitions entre eux. Votre système doit traiter LIVRE, ANNULEE et RETOUR_TERMINEE comme des états terminaux : aucun évènement ne suit.
CodeLibelléCe que cela signifie pour vous
CREEECrééeL'expédition est enregistrée et attend l'enlèvement.
RAMASSERamasséLe colis a été récupéré au point d'enlèvement.
EN_AGENCE_TRIEn agence de triLe colis est en cours d'acheminement dans notre réseau.
EN_LIVRAISONEn cours de livraisonLe colis est confié au livreur pour la tournée du jour.
LIVRELivréRemis au destinataire. État terminal.
REFUSERefuséLe destinataire a refusé le colis. Un retour s'enclenche.
RETOUR_EN_COURSRetour en coursLe colis fait route vers l'expéditeur.
RETOUR_TERMINEERetour terminéeLe retour est clos. État terminal.
ANNULEEAnnuléeL'expédition ne sera pas acheminée. État terminal.
Ne codez pas en dur cette liste. Le catalogue est exposé par l'API (GET /catalog/statuts). Chargez-le au démarrage de votre application pour rester compatible si un statut est ajouté.

Obtenir un jeton

POST /auth/token aucun jeton requis

Échange vos identifiants permanents contre un jeton temporaire. C'est le seul endpoint qui ne demande pas de jeton.

Exemple d'appel

POST /auth/token
Content-Type: application/json

{
  "codeClient": "VOTRE_CODE_CLIENT",
  "secret": "VOTRE_SECRET"
}

Ce que vous envoyez

ChampTypeRequisDescription
codeClienttexteouiVotre identifiant client, fourni par LVE.
secrettexteouiVotre secret, fourni une seule fois à l'ouverture du compte.

Ce que vous recevez

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiJ9…",
    "tokenType": "Bearer",
    "expiresInSeconds": 1800
  }
}

Ce que ça déclenche

Aucun effet métier. Le jeton porte vos droits : présentez-le sur chaque appel suivant dans l'en-tête Authorization: Bearer <accessToken>.

  • Durée de validité : 30 minutes par défaut. Renouvelez avant expiration, ou à la première réponse 401.
  • Il n'y a pas de refresh token : on redemande un jeton avec les mêmes identifiants.
  • Secret erroné → 401. Compte inconnu ou désactivé → 403.

Créer une expédition

POST /demandes demandes:create

Enregistre une expédition : un point d'enlèvement, un destinataire, une marchandise. C'est l'appel central de l'intégration.

Ce que vous envoyez — identification des parties

Le destinataire et le point d'enlèvement s'expriment de deux façons, au choix, indépendamment l'un de l'autre :

  • Par référence, si la partie existe déjà chez nous — destinataire : clientId, point d'enlèvement : id.
  • En clair, en décrivant la partie dans la requête. Elle est alors créée au passage et réutilisable ensuite.
Le nom du champ diffère entre les deux : destinataire.clientId mais pointRamassage.id. C'est la source d'erreur la plus fréquente au démarrage — un champ inconnu est ignoré silencieusement, et la partie part vide.

Ce que vous envoyez — description en clair

ChampTypeDescription
nomtexteRaison sociale ou nom de la partie.
adressetexteAdresse d'enlèvement ou de livraison.
villeentierCode ville du référentiel LVE, transmis à l'onboarding.
telephonetexteFormat marocain : 06…, 05…, 07… ou +212….
mailtexteOptionnel.
clientTypetexteDestinataire uniquement : LVE_D livraison à domicile, LVE_G retrait en agence.

Le point d'enlèvement utilise contactNom, contactTelephone et contactMail à la place de nom, telephone et mail pour la personne à joindre sur place.

Ce que vous envoyez — marchandise et service

ChampTypeRequisRègle
poidsdécimalouiMinimum 2. En kilogrammes.
porttexteouiP port payé par l'expéditeur, D port dû par le destinataire.
livraisontexteouiD à domicile, G retrait en agence.
naturetexteouiNormal, Fragile ou Tres fragile.
colisentiernonNombre de colis. Zéro ou plus.
palettesentiernonAvec le détail par format : paletteA, paletteB, paletteC, paletteAutre.
especedécimalnonMontant à encaisser en espèces à la livraison.
chequedécimalnonMontant à encaisser par chèque.
traitedécimalnonMontant à encaisser par traite.
valeurdécimalnonValeur déclarée de la marchandise.
longueurdécimalnonAvec largeur et hauteur, pour l'encombrement.
bltextenonRéférence de votre bon de livraison, avec nbreBl.
commentairetextenonConsigne libre pour l'exploitation.
numeroSuivitextenonVotre propre référence, reprise telle quelle.

Exemple d'appel

POST /demandes
Authorization: Bearer <accessToken>
Content-Type: application/json

{
  "destinataire": {
    "nom": "Société Exemple",
    "telephone": "0600000000",
    "adresse": "12 rue de l'Exemple",
    "ville": 100,
    "clientType": "LVE_D"
  },
  "pointRamassage": {
    "nom": "Entrepôt Nord",
    "adresse": "Zone industrielle, lot 4",
    "ville": 100,
    "contactNom": "Service expédition",
    "contactTelephone": "0600000000"
  },
  "colis": 1,
  "poids": 2,
  "port": "P",
  "livraison": "D",
  "nature": "Normal"
}

Ce que vous recevez

{
  "success": true,
  "data": {
    "numero": "…",
    "numeroSuivi": "…"
  }
}
  • 201 — l'expédition vient d'être créée.
  • 200 avec le message « Commande deja existante » — requête identique déjà traitée, on vous rend la même expédition.

Ce que ça déclenche

  • L'expédition entre en circuit au statut CREEE et un enlèvement est programmé.
  • Les parties décrites en clair sont enregistrées et réutilisables par référence ensuite.
  • Conservez numero. C'est la clé de tous les appels suivants — suivi, détail, retour.
Cet appel produit un enlèvement réel. Utilisez le compte de test fourni par LVE pendant vos développements ; sur le compte de production, chaque appel engage une exploitation physique.

Créer un lot groupé

POST /demandes-groupees demandes:create

Plusieurs destinations enlevées en une seule fois, au même endroit. Un seul passage de véhicule, autant d'expéditions que de destinations.

Exemple d'appel

POST /demandes-groupees
Authorization: Bearer <accessToken>
Content-Type: application/json

{
  "referenceClient": "LOT-2026-001",
  "pointRamassage": { "id": 25 },
  "declarations": [
    {
      "declaration": {
        "destinataire": { "clientId": 1234 },
        "colis": 1,
        "poids": 2,
        "port": "P",
        "livraison": "D",
        "nature": "Normal"
      }
    }
  ]
}

Ce que vous envoyez

ChampTypeRequisDescription
referenceClienttexteouiVotre référence de lot. Sert de clé anti-doublon.
pointRamassageobjetouiFourni une seule fois pour tout le lot.
declarationslisteouiDe 1 à 100 entrées, chacune de la forme { "declaration": { … } }.

Chaque declaration reprend exactement les champs de la création simple — destinataire, poids, port, livraison, nature, encaissements — sans le point d'enlèvement, porté par le lot.

Ce que vous recevez

{
  "success": true,
  "data": {
    "referenceClient": "LOT-2026-001",
    "codeRamassage": "…",
    "declarations": [
      { "reference": "…", "numero": "…", "numeroSuivi": "…", "complement": null },
      { "reference": "…", "numero": "…", "numeroSuivi": "…", "complement": "…" }
    ],
    "alreadyExisted": false
  }
}
  • La première entrée est la déclaration principale ; son complement est vide.
  • Les suivantes portent dans complement le numéro de la principale : c'est ce qui les rattache au lot.
  • 201 à la création, 200 avec « Lot deja existant » si la même referenceClient a déjà été traitée.

Ce que ça déclenche

  • Un enlèvement unique est programmé pour l'ensemble du lot.
  • Chaque destination devient une expédition autonome, avec son propre numéro, son propre suivi et sa propre facturation.
  • Conservez le numero de la principale : il donne accès au suivi de tout le lot en un appel.
Un lot n'est pas une expédition à plusieurs colis. Groupez quand les destinations diffèrent. Pour plusieurs colis vers une même adresse, utilisez POST /demandes avec colis supérieur à 1.

Lister ses expéditions

GET /demandes demandes:read

Parcourt vos expéditions, de la plus récente à la plus ancienne.

Exemple d'appel

GET /demandes?since=2026-07-01T00:00:00&page=0&size=50
Authorization: Bearer <accessToken>

Ce que vous envoyez

ParamètreTypeDéfautDescription
sincedate-heure—Ne renvoie que les expéditions créées après cette date.
untildate-heure—Borne haute de la période.
pageentier0Index de page, commence à zéro.
sizeentier50Nombre d'éléments par page.

GET /demandes?since=2026-07-01T00:00:00&until=2026-07-31T23:59:59&page=0&size=50

Ce que vous recevez

Une liste allégée : numero, numeroSuivi, date, colis, palettes, nature. Pour tout le reste, appelez le détail.

Ce que ça déclenche

Lecture seule. Utile pour réconcilier votre base avec la nôtre — un inventaire périodique, pas une boucle de suivi : pour l'avancement, préférez les statuts.

Consulter le détail

GET /demandes/{numero} demandes:read

La vue complète d'une expédition : ce qui a été déclaré, et où elle en est.

Exemple d'appel

GET /demandes/123456789
Authorization: Bearer <accessToken>

Ce que vous envoyez

Rien d'autre que le numero dans l'URL, et votre jeton.

Ce que vous recevez

BlocContenu
headerTout ce qui a été déclaré : numéros, date, poids, colis et palettes, port, livraison, nature, encaissements, plus le destinataire et le point d'enlèvement.
colisLa liste des colis rattachés, avec leur numéro.
timelineL'historique complet des statuts, du plus ancien au plus récent.
statutCourantLe statut en vigueur — celui à afficher à vos utilisateurs.

Ce que ça déclenche

Lecture seule. Un 404 signifie que le numéro n'existe pas ou qu'il ne vous appartient pas — les deux cas sont volontairement indiscernables.

Suivre les statuts

GET /demandes/{numero}/statuts statuts:read

L'endpoint de suivi. C'est ici que votre système vient chercher l'avancement, à la fréquence qui vous convient.

Exemple d'appel

GET /demandes/123456789/statuts?since=2026-07-20T09:00:00
Authorization: Bearer <accessToken>

Ce que vous envoyez

ParamètreTypeDescription
sincedate-heureOptionnel. Ne renvoie que les évènements postérieurs à cette date.

Ce que vous recevez

{
  "success": true,
  "data": [
    { "statutCode": "CREEE",   "libelle": "Créée",   "date": "2026-07-20T09:12:00" },
    { "statutCode": "RAMASSE", "libelle": "Ramassé", "date": "2026-07-20T14:03:00" }
  ]
}

Les évènements sont ordonnés du plus ancien au plus récent : le dernier de la liste est le statut courant. Un champ sourceApp accompagne chaque évènement ; il est purement informatif et ne doit pas servir de clé métier.

Ce que ça déclenche

Lecture seule.

  • Cadence conseillée : toutes les 15 à 30 minutes par expédition active. En dessous, vous consommez votre quota sans rien gagner — les statuts changent à l'échelle de l'heure.
  • Utilisez since. Mémorisez la date du dernier évènement reçu et repassez-la : vous ne retraitez pas l'historique à chaque tour.
  • Arrêtez d'interroger une expédition arrivée sur un état terminal — LIVRE, ANNULEE, RETOUR_TERMINEE.

Suivre un lot entier

GET /demandes-groupees/{numeroPrincipal}/statuts statuts:read

Le suivi de toutes les destinations d'un lot en un seul appel, à partir du numéro de la déclaration principale.

Exemple d'appel

GET /demandes-groupees/123456789/statuts
Authorization: Bearer <accessToken>

Ce que vous envoyez

Le numeroPrincipal dans l'URL — le numero de la première déclaration renvoyée à la création du lot. Le paramètre since est accepté, avec le même effet que sur le suivi unitaire.

Ce que vous recevez

{
  "success": true,
  "data": {
    "numeroPrincipal": "…",
    "declarations": [
      {
        "numero": "…",
        "principale": true,
        "complement": null,
        "statutCourant": "EN_LIVRAISON",
        "timeline": [ … ]
      }
    ]
  }
}

La principale et toutes les déclarations qui lui sont rattachées, chacune avec son propre statut courant et son propre historique.

Ce que ça déclenche

Lecture seule. Pour un lot de 40 destinations, préférez systématiquement cet appel aux 40 appels unitaires : c'est une requête au lieu de quarante contre votre quota.

Les destinations d'un lot avancent indépendamment. Une peut être livrée quand une autre est encore en tri. Il n'existe pas de « statut du lot ».

Demander un retour

POST /demandes/{numero}/retour demandes:create

Demande le renvoi d'une expédition vers son expéditeur, à votre initiative.

Exemple d'appel

POST /demandes/123456789/retour
Authorization: Bearer <accessToken>
Content-Type: application/json

{
  "observation": "Retour demandé par le client"
}

Ce que vous envoyez

ChampTypeRequisDescription
observationtextenonMotif du retour, 500 caractères maximum. Transmis à l'exploitation.

Ce que vous recevez

{
  "success": true,
  "data": {
    "numero": "…",
    "alreadyRequested": false
  }
}
  • 202 — la demande est acceptée et sera traitée par l'exploitation.
  • 200 avec alreadyRequested: true — un retour était déjà demandé. Rejouer l'appel est sans risque.

Ce que ça déclenche

  • L'expédition bascule en RETOUR_EN_COURS, puis RETOUR_TERMINEE une fois le colis revenu.
  • Le suivi reste sur le numéro d'origine. Aucune nouvelle référence n'est créée de votre côté : continuez à interroger le même numero.
  • Une expédition déjà livrée ne peut plus être retournée par ce canal — passez par le service client.

Annuler une demande

POST /demandes/{numero}/annuler demandes:create

Permet d'annuler une expédition que vous avez créée, avant sa prise en charge.

Exemple d'appel

POST /demandes/123456789/annuler
Authorization: Bearer <accessToken>
Content-Type: application/json

Ce que vous envoyez

Aucun corps de requête n'est nécessaire. Le numéro suffit.

Ce que vous recevez

{
  "success": true,
  "data": null,
  "message": "Demande d'annulation acceptée"
}
  • 202 — la demande est acceptée et l'annulation est enregistrée.

Ce que ça déclenche

  • L'expédition bascule au statut ANNULEE.
  • Attention : Une expédition déjà prise en charge (ramassée) ou expédiée ne peut plus être annulée par ce biais.

Catalogue des statuts

GET /catalog/statuts jeton valide

La liste de référence des statuts et de leurs libellés.

Exemple d'appel

GET /catalog/statuts
Authorization: Bearer <accessToken>

Ce que vous envoyez

Rien. Un jeton valide suffit, aucun droit particulier n'est exigé.

Ce que vous recevez

{
  "success": true,
  "data": [
    { "code": "CREEE", "libelle": "Créée" },
    { "code": "RAMASSE", "libelle": "Ramassé" }
  ]
}

Ce que ça déclenche

Lecture seule. Appelez-le au démarrage de votre application et gardez le résultat en mémoire : vos écrans afficheront les bons libellés même si le catalogue évolue.

Erreurs

Toutes les erreurs partagent la même enveloppe. Le champ message est destiné à vos journaux techniques : il est explicite, mais ne le montrez pas tel quel à vos utilisateurs finaux.

{
  "success": false,
  "message": "poids doit etre superieur ou egal a 2"
}
CodeSignificationQue faire
400Requête invalide : champ manquant, valeur hors règle, JSON malformé.Corriger la requête. Inutile de réessayer à l'identique.
401Jeton absent, expiré ou invalide.Redemander un jeton, puis rejouer l'appel une fois.
403Droit manquant, ou appel depuis une adresse IP non autorisée.Ne pas réessayer. Contacter LVE pour ajuster le compte.
404Référence inconnue, ou n'appartenant pas à votre compte.Vérifier le numéro conservé à la création.
405Méthode HTTP incorrecte sur cette route.Corriger le verbe HTTP.
409Conflit avec l'état actuel de l'expédition.Relire l'état courant avant de réessayer.
413Charge utile trop volumineuse.Réduire la taille du lot envoyé.
415En-tête Content-Type absent ou incorrect.Envoyer application/json.
429Quota de requêtes par minute dépassé.Attendre la minute suivante, puis reprendre en espaçant les appels.
500Erreur interne de notre côté.Réessayer avec un délai croissant. Signaler si cela persiste.
502Une opération en aval n'a pas abouti.Réessayer : les créations sont rejouables sans risque de doublon.
Politique de reprise conseillée. Réessayez sur 429, 500 et 502, avec un délai qui double à chaque tentative et un plafond de cinq essais. Ne réessayez jamais sur 400, 403 ou 404 : la réponse ne changera pas.