Medusa.js v2 plugin for Boxtal: relay point search, shipping orders, labels, tracking, and fulfillment provider (Mondial Relay / Chronopost via Boxtal API v3).
Plugin Medusa.js v2 pour Boxtal API v3 :
Compatible Medusa ≥ 2.12.
1npm install medusa-plugin-boxtal-v22# ou3yarn add medusa-plugin-boxtal-v21# dans medusa-plugin-boxtal-v22npm run build3npx medusa plugin:publish4
5# dans votre app Medusa6npx medusa plugin:add medusa-plugin-boxtal-v2Ou dépendance fichier :
1{2 "dependencies": {3 "medusa-plugin-boxtal-v2": "file:../medusa-plugin-boxtal-v2"4 }5}Deux enregistrements sont obligatoires dans :
1import { defineConfig, loadEnv } from "@medusajs/framework/utils"2
3loadEnv(process.env.NODE_ENV || "development", process.cwd())4
5module.exports = defineConfig({6 plugins: [7 {8 resolve: "medusa-plugin-boxtal-v2",9 options: {},10 },11 ],12 modules: [13 {14 resolve: "@medusajs/medusa/fulfillment",15 options: {16 providers: [17 {18 resolve: "@medusajs/medusa/fulfillment-manual",19 id: "manual",20 },21 {22 resolve: "medusa-plugin-boxtal-v2/providers/boxtal",23 id: "boxtal",24 options: {25 accessKey: process.env.BOXTAL_ACCESS_KEY,26 secretKey: process.env.BOXTAL_SECRET_KEY,27 environment: process.env.BOXTAL_ENVIRONMENT || "sandbox",28 apiBaseUrl: process.env.BOXTAL_API_BASE_URL,29 relayOfferCode: process.env.BOXTAL_RELAY_OFFER_CODE,30 homeOfferCode: process.env.BOXTAL_HOME_OFFER_CODE,31 relayName: process.env.BOXTAL_RELAY_NAME,32 relayTypeLabel: process.env.BOXTAL_RELAY_TYPE_LABEL,33 relayDescription: process.env.BOXTAL_RELAY_DESCRIPTION,34 homeName: process.env.BOXTAL_HOME_NAME,35 homeTypeLabel: process.env.BOXTAL_HOME_TYPE_LABEL,36 homeDescription: process.env.BOXTAL_HOME_DESCRIPTION,37 labelType: process.env.BOXTAL_LABEL_TYPE || "PDF_A4",38 contentCategoryId: process.env.BOXTAL_CONTENT_CATEGORY_ID,39 contentDescription: process.env.BOXTAL_CONTENT_DESCRIPTION,40 sender: {41 firstName: process.env.BUSINESS_FIRSTNAME,42 lastName: process.env.BUSINESS_LASTNAME,43 street: process.env.BUSINESS_STREET,44 houseNo: process.env.BUSINESS_HOUSE_NO,45 countryCode: process.env.BUSINESS_COUNTRY_CODE || "FR",46 postcode: process.env.BUSINESS_POSTCODE,47 city: process.env.BUSINESS_CITY,48 phone: process.env.BUSINESS_PHONE,49 email: process.env.BUSINESS_EMAIL,50 company: process.env.BUSINESS_COMPANY,51 },52 },53 },54 ],55 },56 },57 ],58})Medusa compose → .
Utilisez cet ID pour détecter les options shipping côté storefront :
option.provider_id?.includes("boxtal")Copiez dans le de votre backend Medusa :
1# --- Boxtal API ---2BOXTAL_ACCESS_KEY=3BOXTAL_SECRET_KEY=4BOXTAL_ENVIRONMENT=sandbox5# Production : https://api.boxtal.com | Sandbox : https://api.boxtal.build6BOXTAL_API_BASE_URL=https://api.boxtal.build7
8# Offres (codes fournis par Boxtal)9BOXTAL_RELAY_OFFER_CODE=MONR-CpourToi10BOXTAL_HOME_OFFER_CODE=CHRP-Chrono1811
12# Libellés checkout (optionnel)13BOXTAL_RELAY_NAME=Mondial Relay - Livraison en point Relais14BOXTAL_RELAY_TYPE_LABEL=Point Relais15BOXTAL_RELAY_DESCRIPTION=Livraison en point relais — 3 à 5 jours ouvrés16BOXTAL_HOME_NAME=Chronopost - Livraison à domicile17BOXTAL_HOME_TYPE_LABEL=Domicile18BOXTAL_HOME_DESCRIPTION=Livraison à domicile — 1 jour ouvré19
20# Tarifs flat (euros) utilisés par le script setup21BOXTAL_RELAY_PRICE=5.922BOXTAL_HOME_PRICE=7.923
24# Étiquette & contenu colis25BOXTAL_LABEL_TYPE=PDF_A426BOXTAL_CONTENT_CATEGORY_ID=content:v1:8050027BOXTAL_CONTENT_DESCRIPTION=Articles de décoration artisanale28
29# Valeur déclarée : items (marchandises) | order_total (total payé)30BOXTAL_DECLARED_VALUE_MODE=items31
32# Fallback dimensions si produit sans L/W/H (cm)33BOXTAL_DEFAULT_PACKAGE_LENGTH_CM=3034BOXTAL_DEFAULT_PACKAGE_WIDTH_CM=2035BOXTAL_DEFAULT_PACKAGE_HEIGHT_CM=1036
37# Webhooks38BOXTAL_WEBHOOK_SECRET=39BOXTAL_WEBHOOK_CALLBACK_URL=https://votre-domaine.com/hooks/boxtal40
41# Expéditeur (obligatoire pour créer une shipping-order)42BUSINESS_FIRSTNAME=43BUSINESS_LASTNAME=44BUSINESS_COMPANY=45BUSINESS_STREET=46BUSINESS_HOUSE_NO=47BUSINESS_POSTCODE=48BUSINESS_CITY=49BUSINESS_COUNTRY_CODE=FR50BUSINESS_PHONE=51BUSINESS_EMAIL=52
53# Requis pour le calcul poids/dims à l’expédition54DATABASE_URL=Après configuration, créez les 2 options (relais + domicile) liées au provider :
1# Depuis le code source du plugin (recommandé en monorepo)2cd medusa-plugin-boxtal-v23# Pointer DATABASE_URL vers la DB de l’app, puis :4npx medusa exec ./src/scripts/setup-boxtal-shipping.tsOu copiez dans votre app et exécutez-le avec .
Le script crée :
| Code type | Usage | |
|---|---|---|
| Point relais (sélection obligatoire) | ||
| Livraison à domicile |
Assurez-vous que vos produits ont un shipping profile relié à la même zone France que le script.
1npx medusa exec ./src/scripts/setup-boxtal-webhook.ts2npx medusa exec ./src/scripts/list-boxtal-webhooks.tsÉvénements gérés : (étiquette), (suivi).
Le middleware du plugin active sur pour la vérif HMAC ().
Toutes les routes Store nécessitent le header .
Recherche de points relais.
Query params :
| Param | Type | Description |
|---|---|---|
| string | Code postal (recommandé) | |
| string | Ville | |
| / | number | Origine GPS (tri proximité) |
| string | Optionnel — poids du panier pour filtrer |
Réponse 200 :
1{2 "relayPoints": [3 {4 "id": "71039",5 "code": "71039",6 "name": "TABAC DE LA GARE",7 "address": "12 rue Example",8 "city": "Paris",9 "zipCode": "75001",10 "country": "FR",11 "latitude": 48.86,12 "longitude": 2.34,13 "network": "MONR",14 "distance": 0.4,15 "schedule": ["Lundi - Vendredi : 09:00 – 19:00"],16 "scheduleDetailed": ["Lundi : 09:00 – 12:00, 14:00 – 19:00", "..."]17 }18 ],19 "meta": {20 "totalFound": 12,21 "parcelWeight": 0.5,22 "sortedByProximity": true,23 "searchOrigin": { "latitude": 48.86, "longitude": 2.34, "label": "75001 Paris" }24 }25}Détail d’un point ( = code parcel point).
Mêmes query optionnels : , , .
Réponse 200 :
Webhook Boxtal (pas de publishable key). Body JSON + signature HMAC.
Auth admin requise. Force la récupération étiquette / tracking depuis Boxtal.
Réponse :
1{2 "order_id": "order_01...",3 "synced": true,4 "results": [{ "fulfillment_id": "ful_...", "synced": true, "label_url": "...", "tracking_number": "..." }]5}Après :
1function isBoxtalProvider(providerId?: string | null) {2 return !!providerId?.includes("boxtal")3}4
5function isBoxtalRelayOption(option: {6 provider_id?: string | null7 type?: { code?: string | null }8 data?: { deliveryType?: string }9}) {10 if (!isBoxtalProvider(option.provider_id)) return false11 return (12 option.type?.code === "boxtal-relay" ||13 option.data?.deliveryType === "relay"14 )15}16
17function isBoxtalHomeOption(option: {18 provider_id?: string | null19 type?: { code?: string | null }20 data?: { deliveryType?: string }21}) {22 if (!isBoxtalProvider(option.provider_id)) return false23 return (24 option.type?.code === "boxtal-home" ||25 option.data?.deliveryType === "home"26 )27}1const BACKEND = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!2const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!3
4async function searchRelayPoints(params: {5 zipCode?: string6 city?: string7 latitude?: number8 longitude?: number9 cartId?: string10}) {11 const q = new URLSearchParams()12 if (params.cartId) q.set("cart_id", params.cartId)13 if (params.zipCode) q.set("zipCode", params.zipCode)14 if (params.city) q.set("city", params.city)15 if (params.latitude != null) q.set("latitude", String(params.latitude))16 if (params.longitude != null) q.set("longitude", String(params.longitude))17
18 const res = await fetch(`${BACKEND}/store/boxtal/relay-points?${q}`, {19 headers: {20 "Content-Type": "application/json",21 "x-publishable-api-key": PUBLISHABLE_KEY,22 },23 cache: "no-store",24 })25 const data = await res.json()26 if (!res.ok) throw new Error(data.message || "Erreur points relais")27 return data as { relayPoints: RelayPoint[]; meta?: unknown }28}Next.js : vous pouvez proxifier via côté app pour éviter d’exposer l’URL backend, ou appeler Medusa directement depuis le serveur.
Avant de finaliser le checkout, stockez le choix dans cart.metadata et dans shipping method data.
Point relais :
1const metadata = {2 carrier: "boxtal",3 deliveryType: "relay",4 parcelPointCode: point.code, // code Boxtal (obligatoire)5 relayPointId: point.code,6 relayPointName: point.name,7 relayPointAddress: `${point.address}, ${point.zipCode} ${point.city}`,8 relayPointNetwork: point.network,9}Domicile :
1const metadata = {2 carrier: "boxtal",3 deliveryType: "home",4}1// 1) Mettre à jour le panier2await sdk.store.cart.update(cartId, { metadata: { ...cart.metadata, ...metadata } })3
4// 2) Sélectionner l’option + data pour validateFulfillmentData5await sdk.store.cart.addShippingMethod(cartId, {6 option_id: shippingOptionId, // id Medusa de l’option boxtal-relay ou boxtal-home7 data: {8 carrier: "boxtal",9 deliveryType: metadata.deliveryType, // "relay" | "home"10 parcelPointCode: metadata.parcelPointCode,11 relayPointId: metadata.relayPointId,12 relayPointName: metadata.relayPointName,13 relayPointAddress: metadata.relayPointAddress,14 },15})Le provider valide que / est présent pour .
11. Charger shipping options du cart22. Afficher options Boxtal (relais / domicile)33. Si relais :4 a. Demander code postal (ou utiliser shipping_address)5 b. GET /store/boxtal/relay-points6 c. L’utilisateur choisit un point7 d. setBoxtalShipping (metadata + shipping method data)84. Si domicile :9 a. setBoxtalShipping avec deliveryType "home"105. Continuer paiement → complete cartLire ou :
| Clé | Description |
|---|---|
| | | |
| Nom du point | |
| Adresse formatée | |
| / | Code Boxtal |
Le subscriber copie ces champs du panier vers la commande.
1export type BoxtalRelayPoint = {2 id: string3 code: string4 name: string5 address: string6 city: string7 zipCode: string8 country: string9 latitude?: number10 longitude?: number11 network?: string12 distance?: number13 schedule?: string[] | null14 scheduleDetailed?: string[] | null15}Après fulfillment, l’étiquette peut arriver avec quelques secondes de délai.
1POST /admin/orders/{order_id}/boxtal-shipping/sync2Authorization: Bearer <admin_token>Ou script :
npx medusa exec ./src/scripts/sync-boxtal-label.ts order_01...Données stockées sur le fulfillment () :
| Moment | Action |
|---|---|
| Copie metadata Boxtal cart → order | |
| Copie poids/dims variante→produit vers metadata lignes | |
| Création fulfillment | Appel Boxtal avec colis calculé |
| Webhook / sync | Met à jour label + tracking |
Poids : variante → metadata → produit parent (grammes). Min. 0,1 kg.
Dimensions : variante → metadata → produit → défauts env (cm).
Valeur déclarée : somme des lignes (euros) par défaut.
Renseignez / / / (ou sur chaque variante) dans l’admin.
| Symptôme | Cause probable | Fix |
|---|---|---|
| Options shipping absentes | Setup non exécuté / mauvais provider_id | Relancer |
| Erreur « point relais manquant » | non passé | Vérifier data |
| Colis 0,1 kg | Poids produit / variante vide | Remplir poids (g) en admin |
| Valeur déclarée 1 € | Ancien bug centimes — versions ≥ 0.1.0 corrigées | Utiliser |
| Pas d’étiquette | Webhook local / délai Boxtal | |
| 401 sur Store API | Publishable key manquante | Header |
1npx medusa exec ./src/scripts/test-boxtal-connection.ts2npx medusa exec ./src/scripts/test-boxtal-package-payload.ts order_xxx3npx medusa exec ./src/scripts/test-boxtal-live-shipment.ts order_xxxMIT