Get started
Overview
Icon for Boxtal

Boxtal

Ship via Boxtal with relay points, labels, and tracking

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 (Copy to clipboardDOCUMENT_CREATED, Copy to clipboardTRACKING_CHANGED)
  • Calcul poids / dimensions / valeur déclarée depuis les produits

Compatible Medusa ≥ 2.12.

Table des matières

  1. Installation
  2. Configuration backend
  3. Variables d’environnement
  4. Créer les shipping options
  5. Webhooks
  6. API référence
  7. Intégration storefront
  8. Admin — sync étiquette
  9. Comportement à la commande
  10. Troubleshooting

1. Installation

npm install medusa-plugin-boxtal-v2
# ou
yarn add medusa-plugin-boxtal-v2

Dé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-v2

Ou dépendance fichier :

{
"dependencies": {
"medusa-plugin-boxtal-v2": "file:../medusa-plugin-boxtal-v2"
}
}

2. Configuration backend

Deux enregistrements sont obligatoires dans Copy to clipboardmedusa-config.ts :

  1. Copy to clipboardplugins — charge les routes API, middlewares webhook, subscribers
  2. Copy to clipboardfulfillment.providers — enregistre le provider Copy to clipboardboxtal
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",
},

ID runtime du provider

Medusa compose Copy to clipboard{id}_{identifier} → Copy to clipboardboxtal_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 Copy to clipboard.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

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.ts

Ou copiez Copy to clipboardsrc/scripts/setup-boxtal-shipping.ts dans votre app et exécutez-le avec Copy to clipboardmedusa exec.

Le script crée :

Code type Copy to clipboarddata.deliveryType Usage Copy to clipboardboxtal-relay Copy to clipboardrelay Point relais (sélection obligatoire) Copy to clipboardboxtal-home Copy to clipboardhome Livraison à domicile

Assurez-vous que vos produits ont un shipping profile relié à la même zone France que le script.

4.1 Tarifs calculés (grille de poids + assurance) — v0.2.0

L’API Boxtal v3 n’expose pas de devis : le prix client est calculé localement par le provider (Copy to clipboardcalculatePrice) à partir d’une configuration stockée dans la table Copy to clipboardboxtal_pricing_settings (créée automatiquement) :

  • Copy to clipboardmode : Copy to clipboardflat (prix fixe par option) ou Copy to clipboardgrid (tranches de poids, poids du panier en kg) ;
  • Copy to clipboardrelay / Copy to clipboardhome : Copy to clipboardflatPrice + Copy to clipboardtiers: [{ maxWeightKg, price }] (prix TTC) ;
  • Copy to clipboarddefaultItemWeightKg : poids appliqué aux articles sans poids ;
  • Copy to clipboardinsurance : Copy to clipboardenabled + Copy to clipboardtiers: [{ maxValue, price }] (prix TTC selon la valeur des marchandises).
  • Copy to clipboardrelay.weightMode / Copy to clipboardhome.weightMode : Copy to clipboardreal ou Copy to clipboardvolumetric (poids facturé = max(poids réel, volume du carton / Copy to clipboardvolumetricDivisor, 5000 par défaut)) ;
  • Copy to clipboardpackaging : catalogue de cartons (Copy to clipboardboxes: [{ id, name, lengthCm, widthCm, heightCm, tareWeightKg, maxWeightKg }]), Copy to clipboarddefaultBoxId (dimensions produit manquantes) et Copy to clipboardfillRate. Le plus petit carton qui contient chaque article et le volume total des articles est retenu ; son poids s’ajoute au colis. Une fois la configuration enregistrée, ce carton est aussi utilisé pour le colis déclaré à Boxtal ;
  • Copy to clipboardcoverage : indemnisation incluse par les transporteurs (relais : forfait par colis ; domicile : €/kg plafonné à la valeur des articles et à Copy to clipboardhomeCap). L’assurance n’est proposée que si la valeur des articles la dépasse.

Au-delà de la dernière tranche, le prix de la dernière tranche s’applique.

Pour utiliser ces tarifs, les options doivent être de type calculated :

npx medusa exec ./src/scripts/setup-boxtal-shipping.ts calculated
# ou BOXTAL_PRICING_MODE=grid

Les options existantes d’un autre type sont supprimées puis recréées. Sans cette étape, les options restent Copy to clipboardflat et le comportement v0.1 est inchangé.

Assurance : passez Copy to clipboardinsured: true dans le Copy to clipboarddata de la shipping method (Copy to clipboardPOST /store/carts/:id/shipping-methods). Le prix inclut alors l’assurance, et Copy to clipboardcreateFulfillment envoie Copy to clipboardinsured: true à Boxtal. Les coûts réels Boxtal (Copy to clipboardboxtal_delivery_price_excl_tax, Copy to clipboardboxtal_insurance_price_excl_tax) sont stockés dans les données du fulfillment.

Routes :

  • Copy to clipboardGET /store/boxtal/shipping-quote?cart_id= → Copy to clipboard{ quotes: { relay, home }, insurance: { enabled } }, chaque devis contenant Copy to clipboardtotalWithoutInsurance, Copy to clipboardtotalWithInsurance, Copy to clipboardinsurancePrice, Copy to clipboardweightKg, Copy to clipboarddeclaredValue ;
  • Copy to clipboardGET /admin/boxtal/pricing / Copy to clipboardPOST /admin/boxtal/pricing (Copy to clipboard{ settings }) pour lire / modifier la configuration.

5. Webhooks

  1. Exposez publiquement Copy to clipboardPOST /hooks/boxtal (tunnel Cloudflare / ngrok en local).
  2. Définissez Copy to clipboardBOXTAL_WEBHOOK_SECRET et Copy to clipboardBOXTAL_WEBHOOK_CALLBACK_URL.
  3. 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 : Copy to clipboardDOCUMENT_CREATED (étiquette), Copy to clipboardTRACKING_CHANGED (suivi).

Le middleware du plugin active Copy to clipboardpreserveRawBody sur Copy to clipboard/hooks/boxtal pour la vérif HMAC (Copy to clipboardx-bxt-signature).

6. API référence

Toutes les routes Store nécessitent le header Copy to clipboardx-publishable-api-key.

Copy to clipboardGET /store/boxtal/relay-points

Recherche de points relais.

Query params :

Param Type Description Copy to clipboardzipCode string Code postal (recommandé) Copy to clipboardcity string Ville Copy to clipboardlatitude / Copy to clipboardlongitude number Origine GPS (tri proximité) Copy to clipboardcart_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,

Copy to clipboardGET /store/boxtal/relay-points/:id

Détail d’un point (Copy to clipboardid = code parcel point).

Mêmes query optionnels : Copy to clipboardzipCode, Copy to clipboardcity, Copy to clipboardcart_id.

Réponse 200 : Copy to clipboard{ "relayPoint": { ... } }

Copy to clipboardPOST /hooks/boxtal

Webhook Boxtal (pas de publishable key). Body JSON + signature HMAC.

Copy to clipboardPOST /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 Copy to clipboardGET /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 }

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",
Next.js : vous pouvez proxifier via Copy to clipboard/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 Copy to clipboardparcelPointCode / Copy to clipboardrelayPointId est présent pour Copy to clipboarddeliveryType: "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 cart

7.6 Afficher le point relais après commande

Lire Copy to clipboardorder.metadata ou Copy to clipboardshipping_methods[0].data :

Clé Description Copy to clipboardcarrier Copy to clipboard"boxtal" Copy to clipboarddeliveryType Copy to clipboard"relay" | Copy to clipboard"home" Copy to clipboardrelayPointName Nom du point Copy to clipboardrelayPointAddress Adresse formatée Copy to clipboardparcelPointCode / Copy to clipboardrelayPointId Code Boxtal

Le subscriber Copy to clipboardorder-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 (Copy to clipboarddata) :

  • Copy to clipboardboxtal_order_id
  • Copy to clipboardboxtal_label_url
  • Copy to clipboardboxtal_tracking_number
  • Copy to clipboardboxtal_tracking_url
  • Copy to clipboardcarrier: "boxtal"

9. Comportement à la commande

Moment Action Copy to clipboardorder.placed Copie metadata Boxtal cart → order Copy to clipboardorder.placed Copie poids/dims variante→produit vers metadata lignes Création fulfillment Appel Boxtal Copy to clipboardcreateShippingOrder 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 Copy to clipboardproduct.weight / Copy to clipboardlength / Copy to clipboardwidth / Copy to clipboardheight (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 Copy to clipboardsetup-boxtal-shipping.ts Erreur « point relais manquant » Copy to clipboarddata.parcelPointCode non passé Vérifier Copy to clipboardaddShippingMethod 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 Copy to clipboardBOXTAL_DECLARED_VALUE_MODE=items Pas d’étiquette Webhook local / délai Boxtal Copy to clipboardPOST .../boxtal-shipping/sync 401 sur Store API Publishable key manquante Header Copy to clipboardx-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_xxx

Licence

MIT

You may also like

Browse all integrations

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?