medusa-plugin-boxtal-v2
v0.1.0
Published
Medusa.js v2 plugin for Boxtal: relay point search, shipping orders, labels, tracking, and fulfillment provider (Mondial Relay / Chronopost via Boxtal API v3).
Maintainers
Readme
medusa-plugin-boxtal-v2
Plugin Medusa.js v2 pour Boxtal API v3 :
- Provider de fulfillment (Mondial Relay point relais + Chronopost domicile)
- Recherche / détail de points relais (Store API)
- Création d’expédition, étiquettes PDF, tracking
- Webhooks HMAC (
DOCUMENT_CREATED,TRACKING_CHANGED) - Calcul poids / dimensions / valeur déclarée depuis les produits
Compatible Medusa ≥ 2.12.
Table des matières
- Installation
- Configuration backend
- Variables d’environnement
- Créer les shipping options
- Webhooks
- API référence
- Intégration storefront
- Admin — sync étiquette
- Comportement à la commande
- Troubleshooting
1. Installation
npm install medusa-plugin-boxtal-v2
# ou
yarn add medusa-plugin-boxtal-v2Développement local (yalc)
# dans medusa-plugin-boxtal-v2
npm run build
npx medusa plugin:publish
# dans votre app Medusa
npx medusa plugin:add medusa-plugin-boxtal-v2Ou dépendance fichier :
{
"dependencies": {
"medusa-plugin-boxtal-v2": "file:../medusa-plugin-boxtal-v2"
}
}2. Configuration backend
Deux enregistrements sont obligatoires dans medusa-config.ts :
plugins— charge les routes API, middlewares webhook, subscribersfulfillment.providers— enregistre le providerboxtal
import { defineConfig, loadEnv } from "@medusajs/framework/utils"
loadEnv(process.env.NODE_ENV || "development", process.cwd())
module.exports = defineConfig({
plugins: [
{
resolve: "medusa-plugin-boxtal-v2",
options: {},
},
],
modules: [
{
resolve: "@medusajs/medusa/fulfillment",
options: {
providers: [
{
resolve: "@medusajs/medusa/fulfillment-manual",
id: "manual",
},
{
resolve: "medusa-plugin-boxtal-v2/providers/boxtal",
id: "boxtal",
options: {
accessKey: process.env.BOXTAL_ACCESS_KEY,
secretKey: process.env.BOXTAL_SECRET_KEY,
environment: process.env.BOXTAL_ENVIRONMENT || "sandbox",
apiBaseUrl: process.env.BOXTAL_API_BASE_URL,
relayOfferCode: process.env.BOXTAL_RELAY_OFFER_CODE,
homeOfferCode: process.env.BOXTAL_HOME_OFFER_CODE,
relayName: process.env.BOXTAL_RELAY_NAME,
relayTypeLabel: process.env.BOXTAL_RELAY_TYPE_LABEL,
relayDescription: process.env.BOXTAL_RELAY_DESCRIPTION,
homeName: process.env.BOXTAL_HOME_NAME,
homeTypeLabel: process.env.BOXTAL_HOME_TYPE_LABEL,
homeDescription: process.env.BOXTAL_HOME_DESCRIPTION,
labelType: process.env.BOXTAL_LABEL_TYPE || "PDF_A4",
contentCategoryId: process.env.BOXTAL_CONTENT_CATEGORY_ID,
contentDescription: process.env.BOXTAL_CONTENT_DESCRIPTION,
sender: {
firstName: process.env.BUSINESS_FIRSTNAME,
lastName: process.env.BUSINESS_LASTNAME,
street: process.env.BUSINESS_STREET,
houseNo: process.env.BUSINESS_HOUSE_NO,
countryCode: process.env.BUSINESS_COUNTRY_CODE || "FR",
postcode: process.env.BUSINESS_POSTCODE,
city: process.env.BUSINESS_CITY,
phone: process.env.BUSINESS_PHONE,
email: process.env.BUSINESS_EMAIL,
company: process.env.BUSINESS_COMPANY,
},
},
},
],
},
},
],
})ID runtime du provider
Medusa compose {id}_{identifier} → boxtal_boxtal.
Utilisez cet ID pour détecter les options shipping côté storefront :
option.provider_id?.includes("boxtal")3. Variables d’environnement
Copiez dans le .env de votre backend Medusa :
# --- Boxtal API ---
BOXTAL_ACCESS_KEY=
BOXTAL_SECRET_KEY=
BOXTAL_ENVIRONMENT=sandbox
# Production : https://api.boxtal.com | Sandbox : https://api.boxtal.build
BOXTAL_API_BASE_URL=https://api.boxtal.build
# Offres (codes fournis par Boxtal)
BOXTAL_RELAY_OFFER_CODE=MONR-CpourToi
BOXTAL_HOME_OFFER_CODE=CHRP-Chrono18
# Libellés checkout (optionnel)
BOXTAL_RELAY_NAME=Mondial Relay - Livraison en point Relais
BOXTAL_RELAY_TYPE_LABEL=Point Relais
BOXTAL_RELAY_DESCRIPTION=Livraison en point relais — 3 à 5 jours ouvrés
BOXTAL_HOME_NAME=Chronopost - Livraison à domicile
BOXTAL_HOME_TYPE_LABEL=Domicile
BOXTAL_HOME_DESCRIPTION=Livraison à domicile — 1 jour ouvré
# Tarifs flat (euros) utilisés par le script setup
BOXTAL_RELAY_PRICE=5.9
BOXTAL_HOME_PRICE=7.9
# Étiquette & contenu colis
BOXTAL_LABEL_TYPE=PDF_A4
BOXTAL_CONTENT_CATEGORY_ID=content:v1:80500
BOXTAL_CONTENT_DESCRIPTION=Articles de décoration artisanale
# Valeur déclarée : items (marchandises) | order_total (total payé)
BOXTAL_DECLARED_VALUE_MODE=items
# Fallback dimensions si produit sans L/W/H (cm)
BOXTAL_DEFAULT_PACKAGE_LENGTH_CM=30
BOXTAL_DEFAULT_PACKAGE_WIDTH_CM=20
BOXTAL_DEFAULT_PACKAGE_HEIGHT_CM=10
# Webhooks
BOXTAL_WEBHOOK_SECRET=
BOXTAL_WEBHOOK_CALLBACK_URL=https://votre-domaine.com/hooks/boxtal
# Expéditeur (obligatoire pour créer une shipping-order)
BUSINESS_FIRSTNAME=
BUSINESS_LASTNAME=
BUSINESS_COMPANY=
BUSINESS_STREET=
BUSINESS_HOUSE_NO=
BUSINESS_POSTCODE=
BUSINESS_CITY=
BUSINESS_COUNTRY_CODE=FR
BUSINESS_PHONE=
BUSINESS_EMAIL=
# Requis pour le calcul poids/dims à l’expédition
DATABASE_URL=4. Créer les shipping options
Après configuration, créez les 2 options (relais + domicile) liées au provider :
# Depuis le code source du plugin (recommandé en monorepo)
cd medusa-plugin-boxtal-v2
# Pointer DATABASE_URL vers la DB de l’app, puis :
npx medusa exec ./src/scripts/setup-boxtal-shipping.tsOu copiez src/scripts/setup-boxtal-shipping.ts dans votre app et exécutez-le avec medusa exec.
Le script crée :
| Code type | data.deliveryType | Usage |
|-----------|---------------------|--------|
| boxtal-relay | relay | Point relais (sélection obligatoire) |
| boxtal-home | home | Livraison à domicile |
Assurez-vous que vos produits ont un shipping profile relié à la même zone France que le script.
5. Webhooks
- Exposez publiquement
POST /hooks/boxtal(tunnel Cloudflare / ngrok en local). - Définissez
BOXTAL_WEBHOOK_SECRETetBOXTAL_WEBHOOK_CALLBACK_URL. - Enregistrez les subscriptions :
npx medusa exec ./src/scripts/setup-boxtal-webhook.ts
npx medusa exec ./src/scripts/list-boxtal-webhooks.tsÉvénements gérés : DOCUMENT_CREATED (étiquette), TRACKING_CHANGED (suivi).
Le middleware du plugin active preserveRawBody sur /hooks/boxtal pour la vérif HMAC (x-bxt-signature).
6. API référence
Toutes les routes Store nécessitent le header x-publishable-api-key.
GET /store/boxtal/relay-points
Recherche de points relais.
Query params :
| Param | Type | Description |
|-------|------|-------------|
| zipCode | string | Code postal (recommandé) |
| city | string | Ville |
| latitude / longitude | number | Origine GPS (tri proximité) |
| cart_id | string | Optionnel — poids du panier pour filtrer |
Réponse 200 :
{
"relayPoints": [
{
"id": "71039",
"code": "71039",
"name": "TABAC DE LA GARE",
"address": "12 rue Example",
"city": "Paris",
"zipCode": "75001",
"country": "FR",
"latitude": 48.86,
"longitude": 2.34,
"network": "MONR",
"distance": 0.4,
"schedule": ["Lundi - Vendredi : 09:00 – 19:00"],
"scheduleDetailed": ["Lundi : 09:00 – 12:00, 14:00 – 19:00", "..."]
}
],
"meta": {
"totalFound": 12,
"parcelWeight": 0.5,
"sortedByProximity": true,
"searchOrigin": { "latitude": 48.86, "longitude": 2.34, "label": "75001 Paris" }
}
}GET /store/boxtal/relay-points/:id
Détail d’un point (id = code parcel point).
Mêmes query optionnels : zipCode, city, cart_id.
Réponse 200 : { "relayPoint": { ... } }
POST /hooks/boxtal
Webhook Boxtal (pas de publishable key). Body JSON + signature HMAC.
POST /admin/orders/:id/boxtal-shipping/sync
Auth admin requise. Force la récupération étiquette / tracking depuis Boxtal.
Réponse :
{
"order_id": "order_01...",
"synced": true,
"results": [{ "fulfillment_id": "ful_...", "synced": true, "label_url": "...", "tracking_number": "..." }]
}7. Intégration storefront
7.1 Détecter les options Boxtal
Après GET /store/shipping-options?cart_id=... :
function isBoxtalProvider(providerId?: string | null) {
return !!providerId?.includes("boxtal")
}
function isBoxtalRelayOption(option: {
provider_id?: string | null
type?: { code?: string | null }
data?: { deliveryType?: string }
}) {
if (!isBoxtalProvider(option.provider_id)) return false
return (
option.type?.code === "boxtal-relay" ||
option.data?.deliveryType === "relay"
)
}
function isBoxtalHomeOption(option: {
provider_id?: string | null
type?: { code?: string | null }
data?: { deliveryType?: string }
}) {
if (!isBoxtalProvider(option.provider_id)) return false
return (
option.type?.code === "boxtal-home" ||
option.data?.deliveryType === "home"
)
}7.2 Client HTTP (exemple)
const BACKEND = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!
const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!
async function searchRelayPoints(params: {
zipCode?: string
city?: string
latitude?: number
longitude?: number
cartId?: string
}) {
const q = new URLSearchParams()
if (params.cartId) q.set("cart_id", params.cartId)
if (params.zipCode) q.set("zipCode", params.zipCode)
if (params.city) q.set("city", params.city)
if (params.latitude != null) q.set("latitude", String(params.latitude))
if (params.longitude != null) q.set("longitude", String(params.longitude))
const res = await fetch(`${BACKEND}/store/boxtal/relay-points?${q}`, {
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": PUBLISHABLE_KEY,
},
cache: "no-store",
})
const data = await res.json()
if (!res.ok) throw new Error(data.message || "Erreur points relais")
return data as { relayPoints: RelayPoint[]; meta?: unknown }
}Next.js : vous pouvez proxifier via
/api/store/boxtal/...côté app pour éviter d’exposer l’URL backend, ou appeler Medusa directement depuis le serveur.
7.3 Metadata panier (obligatoire)
Avant de finaliser le checkout, stockez le choix dans cart.metadata et dans shipping method data.
Point relais :
const metadata = {
carrier: "boxtal",
deliveryType: "relay",
parcelPointCode: point.code, // code Boxtal (obligatoire)
relayPointId: point.code,
relayPointName: point.name,
relayPointAddress: `${point.address}, ${point.zipCode} ${point.city}`,
relayPointNetwork: point.network,
}Domicile :
const metadata = {
carrier: "boxtal",
deliveryType: "home",
}7.4 Attacher la shipping method
// 1) Mettre à jour le panier
await sdk.store.cart.update(cartId, { metadata: { ...cart.metadata, ...metadata } })
// 2) Sélectionner l’option + data pour validateFulfillmentData
await sdk.store.cart.addShippingMethod(cartId, {
option_id: shippingOptionId, // id Medusa de l’option boxtal-relay ou boxtal-home
data: {
carrier: "boxtal",
deliveryType: metadata.deliveryType, // "relay" | "home"
parcelPointCode: metadata.parcelPointCode,
relayPointId: metadata.relayPointId,
relayPointName: metadata.relayPointName,
relayPointAddress: metadata.relayPointAddress,
},
})Le provider valide que parcelPointCode / relayPointId est présent pour deliveryType: "relay".
7.5 Flux checkout recommandé
1. Charger shipping options du cart
2. Afficher options Boxtal (relais / domicile)
3. Si relais :
a. Demander code postal (ou utiliser shipping_address)
b. GET /store/boxtal/relay-points
c. L’utilisateur choisit un point
d. setBoxtalShipping (metadata + shipping method data)
4. Si domicile :
a. setBoxtalShipping avec deliveryType "home"
5. Continuer paiement → complete cart7.6 Afficher le point relais après commande
Lire order.metadata ou shipping_methods[0].data :
| Clé | Description |
|-----|-------------|
| carrier | "boxtal" |
| deliveryType | "relay" | "home" |
| relayPointName | Nom du point |
| relayPointAddress | Adresse formatée |
| parcelPointCode / relayPointId | Code Boxtal |
Le subscriber order-placed-boxtal copie ces champs du panier vers la commande.
7.7 Types TypeScript utiles
export type BoxtalRelayPoint = {
id: string
code: string
name: string
address: string
city: string
zipCode: string
country: string
latitude?: number
longitude?: number
network?: string
distance?: number
schedule?: string[] | null
scheduleDetailed?: string[] | null
}8. Admin — sync étiquette
Après fulfillment, l’étiquette peut arriver avec quelques secondes de délai.
POST /admin/orders/{order_id}/boxtal-shipping/sync
Authorization: Bearer <admin_token>Ou script :
npx medusa exec ./src/scripts/sync-boxtal-label.ts order_01...Données stockées sur le fulfillment (data) :
boxtal_order_idboxtal_label_urlboxtal_tracking_numberboxtal_tracking_urlcarrier: "boxtal"
9. Comportement à la commande
| Moment | Action |
|--------|--------|
| order.placed | Copie metadata Boxtal cart → order |
| order.placed | Copie poids/dims variante→produit vers metadata lignes |
| Création fulfillment | Appel Boxtal createShippingOrder 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 product.weight / length / width / height (ou sur chaque variante) dans l’admin.
10. Troubleshooting
| Symptôme | Cause probable | Fix |
|----------|----------------|-----|
| Options shipping absentes | Setup non exécuté / mauvais provider_id | Relancer setup-boxtal-shipping.ts |
| Erreur « point relais manquant » | data.parcelPointCode non passé | Vérifier addShippingMethod 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 BOXTAL_DECLARED_VALUE_MODE=items |
| Pas d’étiquette | Webhook local / délai Boxtal | POST .../boxtal-shipping/sync |
| 401 sur Store API | Publishable key manquante | Header x-publishable-api-key |
Scripts de test (repo plugin)
npx medusa exec ./src/scripts/test-boxtal-connection.ts
npx medusa exec ./src/scripts/test-boxtal-package-payload.ts order_xxx
npx medusa exec ./src/scripts/test-boxtal-live-shipment.ts order_xxxLicence
MIT
