Votre boutique ne tourne ni sur Shopify, ni sur WooCommerce, ni sur Magento ? Nos connecteurs n’utilisent rien que vous ne puissiez utiliser : une API REST, une clé, cinq appels. Ce guide est le chemin le plus court, avec les requêtes à copier.
POST /ordersPOST /orders/{id}/quotesPOST /orders/{id}/shipGET /shipments/{ref}/label.pdfGET /shipments/{ref}Le marchand crée sa clé lui-même, dans son tableau de bord : Paramètres → Clés API → Créer une clé. Elle s’affiche une seule fois. Une clé « shp_live_… » crée de vrais colis ; une clé « shp_test_… » sert à développer. Envoyez-la dans l’en-tête Authorization de chaque requête.
Base URL https://shiplayer.online
Content-Type application/json
Authorization: Bearer shp_live_xxxxxxxxxxxxxxxxxxxxxxxx
# Vérifiez la clé : votre compte marchand
curl https://shiplayer.online/merchants/me \
-H "Authorization: Bearer $SHIPLAYER_KEY"
Toutes les réponses sont en JSON. Les montants sont des chaînes en dirhams (« 38.08 »), jamais des flottants. Les dates sont en UTC, format ISO 8601. La limite est d’environ 10 requêtes par seconde par clé : au-delà, 429, réessayez après une seconde.
La référence complète de chaque route, générée depuis le code : shiplayer.online/docs.
Envoyez la commande dès qu’elle est passée dans votre boutique. Elle arrive dans la boîte « Commandes » du marchand, avec son score de risque et le lien WhatsApp pour la confirmer avec le client — l’expédition se décide ensuite, par vous ou par lui.
curl -X POST https://shiplayer.online/orders \
-H "Authorization: Bearer $SHIPLAYER_KEY" -H "Content-Type: application/json" \
-d '{
"order_id": "CMD-10482",
"origin": {
"raw": "12 rue Ibnou Mounir, Maarif", "city": "Casablanca",
"phone": "0661000000", "recipient_name": "Boutique Atlas"
},
"destination": {
"raw": "Résidence Yasmine, imm. B, apt 4, Guéliz", "city": "Marrakech",
"phone": "0662000000", "recipient_name": "Salma Bennani"
},
"parcel": { "weight_kg": "0.8" },
"payment_mode": "cod",
"cod_amount_mad": "450",
"declared_value_mad": "450",
"ready_at": "2026-10-14T09:00:00",
"contents": "Caftan",
"notes": "Appeler avant livraison"
}'
201 Created
{
"order_id": "CMD-10482",
"recipient": "Salma Bennani", "city": "Marrakech", "zone": "metro",
"cod_mad": "450", "needs_address_review": false,
"risk": { "score": 34, "band": "medium", "action": "confirm_first", ... },
"whatsapp_url": "https://wa.me/212662000000?text=...",
...
}
| Champ | Obligatoire | Notes |
|---|---|---|
order_id | oui | Votre numéro de commande, 64 caractères max. Il doit être unique : le renvoyer répond 409, ce qui rend vos réessais sans danger. |
origin, destination | oui | raw (l’adresse en texte libre), city, phone, recipient_name. Pas de zone à fournir : nous la déduisons de la ville. La ville doit être orthographiée comme les transporteurs la connaissent (« Missour », pas « Mayssour »). |
parcel.weight_kg | oui | Supérieur à 0. Certains transporteurs ne prennent pas les colis trop légers ou trop lourds sur certaines villes : ils n’apparaissent alors pas dans les tarifs. |
payment_mode | oui | cod (contre-remboursement, alors cod_amount_mad est obligatoire) ou prepaid. |
declared_value_mad | oui | La valeur de la marchandise, base de l’indemnisation et de l’assurance. |
ready_at | oui | Quand le colis sera prêt à être ramassé. |
contents, notes | non | Le contenu est imprimé sur l’étiquette ; la remarque est transmise au transporteur. |
exchange_contents | non | Un échange : ce que le livreur reprend chez le client pendant la même visite. |
Facultatif, si vous laissez Shiplayer choisir. Sinon, cet appel donne chaque transporteur qui dessert la ville, avec son prix et son coût réel attendu — le prix plus le risque d’échec et la trésorerie immobilisée, la méthode expliquée sur la page d’accueil. Les transporteurs qui ne desservent pas la ville sont listés dans not_offered, avec la raison.
curl -X POST https://shiplayer.online/orders/CMD-10482/quotes \
-H "Authorization: Bearer $SHIPLAYER_KEY"
200 OK
{
"recommended": { "carrier_code": "relaisexpress", "price_mad": "30.00", ... },
"candidates": [
{ "carrier_code": "relaisexpress", "price_mad": "30.00",
"expected_landed_cost_mad": "48.10", "promised_transit_hours": 48,
"rate_basis": "contract", "insurance_offer_fee_mad": "6.75", ... },
{ "carrier_code": "sendit", "price_mad": "45.00", ... }
],
"not_offered": [
{ "carrier_code": "digylog", "reason": "Digylog n'accepte pas les colis de moins de 1.2 kg." }
]
}
Nommez un transporteur, ou laissez « auto » : Shiplayer prend celui au coût réel le plus bas selon le réglage du marchand. La réponse est le colis complet — gardez shiplayer_ref, c’est la référence de tout le reste, et carrier_tracking_ref, le numéro du transporteur. Le lien de suivi à donner au client est shiplayer.online/t/{shiplayer_ref}.
curl -X POST https://shiplayer.online/orders/CMD-10482/ship \
-H "Authorization: Bearer $SHIPLAYER_KEY" -H "Content-Type: application/json" \
-d '{ "carrier_code": "auto", "insured": false }'
200 OK
{
"shiplayer_ref": "SHP8B064DE84C28",
"order_id": "CMD-10482",
"status": "created",
"total_price_mad": "30.00",
"legs": [ { "carrier_code": "relaisexpress", "carrier_tracking_ref": "RE123456789",
"from_city": "Casablanca", "to_city": "Marrakech", "price_mad": "30.00",
"promised_transit_hours": 48, ... } ],
"events": [ { "at": "2026-10-13T14:18:44Z", "status": "created",
"description": "Expédition créée, étiquette prête" } ],
"pickup": { "stated": true, "date": "2026-10-14", "when": "demain avant 12 h", ... },
"cancellable": true,
...
}
L’étiquette est un PDF au format du transporteur, avec le logo et le contenu. Imprimez-la et collez-la sur le colis. Le ramassage se gère dans le tableau de bord du marchand (Expéditions → Ramassages) : il y voit l’heure du passage et confirme la remise au chauffeur. Pour une annulation, c’est possible tant que le colis n’a pas été ramassé.
# L'étiquette (PDF)
curl https://shiplayer.online/shipments/SHP8B064DE84C28/label.pdf \
-H "Authorization: Bearer $SHIPLAYER_KEY" -o etiquette.pdf
# Plusieurs étiquettes en un PDF
curl "https://shiplayer.online/labels.pdf?ref=SHP8B064DE84C28&ref=SHP2F19C0A7E415" \
-H "Authorization: Bearer $SHIPLAYER_KEY" -o etiquettes.pdf
# Annuler avant ramassage
curl -X POST https://shiplayer.online/shipments/SHP8B064DE84C28/cancel \
-H "Authorization: Bearer $SHIPLAYER_KEY"
Le meilleur moyen est le webhook ci-dessous : chaque changement arrive chez vous, sans rien demander. Sinon, lisez le colis quand vous voulez ; la réponse est la même que celle de l’expédition, avec l’historique complet dans events. La page publique /t/{ref} et son JSON /track/{ref} n’ont pas besoin de clé et ne montrent rien de confidentiel : vous pouvez les donner au client.
curl https://shiplayer.online/shipments/SHP8B064DE84C28 \
-H "Authorization: Bearer $SHIPLAYER_KEY"
200 OK
{
"shiplayer_ref": "SHP8B064DE84C28", "status": "delivered",
"delivered_at": "2026-10-16T11:02:00Z",
"events": [
{ "at": "2026-10-13T14:18:44Z", "status": "created", "description": "Expédition créée, étiquette prête" },
{ "at": "2026-10-14T10:40:12Z", "status": "carrier_received", "description": "Pris en charge", "location": "Casablanca" },
{ "at": "2026-10-15T07:12:30Z", "status": "in_transit", "location": "Marrakech" },
{ "at": "2026-10-16T08:05:00Z", "status": "out_for_delivery" },
{ "at": "2026-10-16T11:02:00Z", "status": "delivered" }
],
"pod": { "recipient_name": "Salma Bennani", "recorded_at": "2026-10-16T11:02:00Z", ... },
...
}
# Sans clé, pour le client
curl https://shiplayer.online/track/SHP8B064DE84C28
Le marchand indique l’adresse de votre système dans Paramètres → Webhook et reçoit un secret, affiché une fois. À partir de là, Shiplayer envoie un POST JSON à cette adresse à chaque création de colis et à chaque changement de statut. Le corps est une enveloppe dont data est exactement l’objet de GET /shipments/{ref}, plus tracking_url : vous n’avez qu’un seul format à connaître.
| Événement | Quand |
|---|---|
shipment.created | Le colis vient d’être créé : il a sa référence, son transporteur et son étiquette. |
shipment.updated | Le statut du colis a changé (pris en charge, en transit, livré, échec, retourné, annulé…). Une fois par changement, pas à chaque scan. |
ping | Le bouton « Tester » du tableau de bord. Répondez 200 et c’est tout. |
POST https://ma-boutique.ma/webhooks/shiplayer
Content-Type: application/json
User-Agent: Shiplayer-Webhooks/1.0
Shiplayer-Event: shipment.updated
Shiplayer-Delivery: whd_3f9c2a7b1e4d5a6c8b0f
Shiplayer-Signature: t=1760364004,v1=5c1a0f…e2b9
{
"id": "whd_3f9c2a7b1e4d5a6c8b0f",
"event": "shipment.updated",
"created_at": "2026-10-16T11:02:04Z",
"merchant_id": "mrc_atlas",
"data": {
"shiplayer_ref": "SHP8B064DE84C28",
"order_id": "CMD-10482",
"status": "delivered",
"tracking_url": "https://shiplayer.online/t/SHP8B064DE84C28",
"legs": [ ... ], "events": [ ... ], "pod": { ... },
...
}
}
Chaque envoi est signé avec le secret du marchand : v1 = HMAC-SHA256(secret, "{t}.{body}"), en hexadécimal. Recalculez-la sur le corps tel que reçu, octet pour octet, avant de le parser ; comparez en temps constant ; refusez un t à plus de cinq minutes de votre horloge. Un envoi dont la signature ne correspond pas ne vient pas de Shiplayer.
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes) -> bool:
fields = dict(p.split("=", 1) for p in header.split(","))
t, v1 = int(fields["t"]), fields["v1"]
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
# Flask
@app.post("/webhooks/shiplayer")
def shiplayer():
if not verify(SECRET, request.headers["Shiplayer-Signature"], request.get_data()):
abort(401)
msg = request.get_json()
if msg["event"] == "shipment.updated":
update_order(msg["data"]["order_id"], msg["data"]["status"], msg["data"]["tracking_url"])
return "", 200
function shiplayer_verify(string $secret, string $header, string $body): bool {
parse_str(str_replace(',', '&', $header), $f);
if (!isset($f['t'], $f['v1']) || abs(time() - (int)$f['t']) > 300) return false;
$expected = hash_hmac('sha256', $f['t'] . '.' . $body, $secret);
return hash_equals($expected, $f['v1']);
}
$body = file_get_contents('php://input');
if (!shiplayer_verify(SECRET, $_SERVER['HTTP_SHIPLAYER_SIGNATURE'] ?? '', $body)) {
http_response_code(401); exit;
}
$msg = json_decode($body, true);
if ($msg['event'] === 'shipment.updated') {
update_order($msg['data']['order_id'], $msg['data']['status'], $msg['data']['tracking_url']);
}
http_response_code(200);
const crypto = require("crypto");
function verify(secret, header, rawBody) {
const f = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(f.t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${f.t}.`).update(rawBody).digest("hex");
return expected.length === f.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(f.v1));
}
// Express — gardez le corps brut : express.raw(), pas express.json()
app.post("/webhooks/shiplayer", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(SECRET, req.get("Shiplayer-Signature") || "", req.body)) return res.sendStatus(401);
const msg = JSON.parse(req.body);
if (msg.event === "shipment.updated") updateOrder(msg.data.order_id, msg.data.status, msg.data.tracking_url);
res.sendStatus(200);
});
Répondez 2xx en moins de dix secondes, puis traitez. Toute autre réponse, ou l’absence de réponse, est réessayée : après 1, 5 et 15 minutes, puis 1, 3, 6, 12 et 24 heures — huit tentatives sur un jour. Un même message peut donc arriver deux fois : gardez son id et ignorez un doublon. L’ordre n’est pas garanti entre deux messages proches : fiez-vous au statut et aux events de data, pas à l’ordre d’arrivée. Le marchand voit chaque envoi, sa réponse et son prochain essai dans Paramètres → Webhook → Journal des envois.
L’adresse doit être en https, sur un serveur public. Pendant le développement, un tunnel (ngrok, Cloudflare Tunnel) vers votre machine fait l’affaire ; le bouton « Tester » envoie un ping tout de suite et affiche la réponse de votre serveur.
Chaque transporteur a ses propres codes ; Shiplayer les traduit tous dans cette liste. Le code d’origine reste dans raw_carrier_code de chaque event si vous en avez besoin.
| status | Sens |
|---|---|
created | Colis créé, étiquette prête, en attente de ramassage. |
picked_up | Le marchand a confirmé la remise au chauffeur. |
carrier_received | Le transporteur a scanné le colis : il est dans son réseau. |
in_transit | En route vers la ville de destination. |
out_for_delivery | Chez le livreur, livraison dans la journée. |
attempt_failed | Tentative échouée (absent, injoignable, refus). Le marchand décide dans son tableau de bord : nouvelle tentative ou retour. |
delivered | Livré. Final. Le contre-remboursement est dû par le transporteur ; pod porte la preuve quand il la fournit. |
returned | Retourné au marchand. Final. |
cancelled | Annulé avant ramassage : rien n’a quitté la boutique. Final. |
lost | Perdu, déclaré par le transporteur. Final ; l’indemnisation s’applique. |
Le corps d’une erreur est toujours un objet avec une clé detail : une phrase en français, ou pour 422 la liste des champs refusés avec leur emplacement.
401 | Clé absente, inconnue ou révoquée. |
404 | La commande ou le colis n’existe pas — ou appartient à un autre marchand, ce qui est la même réponse. |
409 | Conflit : order_id déjà utilisé, commande déjà expédiée, compte pas encore validé, Idempotency-Key réutilisée avec un autre corps. Le message dit lequel. |
422 | Corps invalide : champ manquant, trop long, montant négatif, ville non desservie par aucun transporteur. |
429 | Trop de requêtes : attendez une seconde et réessayez. |
5xx | Shiplayer ou le transporteur n’a pas répondu. Rien n’a été créé : réessayez, avec le même Idempotency-Key si vous passez par POST /shipments. |