Introduction
L'API Kolia est une API REST : des URL prévisibles, des corps JSON, des codes HTTP standard. Toutes les routes sont préfixées par /v1 et servies en HTTPS depuis https://api.kolia.juali.pro.
Les montants sont des entiers en FCFA, arrondis au multiple de 5. Les champs et les statuts sont en français : ils reprennent le vocabulaire du terrain.
Le débit est limité à 300 requêtes par minute par adresse IP ; au-delà, l'API répond 429. Chaque clé a en plus son propre quota journalier.
Environnement de test. Une clé de bac à sable (kolia_tk_) crée des expéditions isolées qui se livrent seules : ni livreur, ni paiement, ni message réels.
Démarrage
- Créez votre compte commerçant sur app.kolia.juali.pro, puis déclarez votre point de collecte.
- Créez une clé dans l'espace commerçant, rubrique Développeurs — réelle ou bac à sable. Elle n'est affichée qu'une seule fois.
- Demandez un devis, créez l'expédition dans les quinze minutes, suivez-la par webhook.
# Devis curl -X POST https://api.kolia.juali.pro/v1/quotes \ -H "X-API-Key: $KOLIA_API_KEY" -H "Content-Type: application/json" \ -d '{"pickupPointId":"b3e1…","destLat":6.3525,"destLng":2.3675,"modePaiement":"PREPAYE"}' # Expédition curl -X POST https://api.kolia.juali.pro/v1/shipments \ -H "X-API-Key: $KOLIA_API_KEY" -H "Content-Type: application/json" \ -d '{"quoteId":"9f0c…","pickupPointId":"b3e1…","destinataireNom":"Afi H.", "destinataireTel":"+22997000000","destAdresse":"Fidjrossè","contenu":"Colis"}'
Authentification
Les codes de vérification des comptes (inscription, mot de passe oublié) partent par e-mail, ou sont générés par une application d'authentification (Google Authenticator, Microsoft Authenticator…). Aucun code n'est envoyé par SMS ni par WhatsApp. Un compte protégé par une application répond à POST /v1/auth/login par { "mfaRequis": true, "jetonMfa": "…" }, à compléter avec le code par POST /v1/auth/mfa/verifier.
| Mécanisme | En-tête | Usage |
|---|---|---|
| Clé API | X-API-Key: kolia_sk_… | Intégrations serveur. Agit au nom du compte commerçant qui l'a créée, dans la limite de ses portées (*, quotes, shipments, tracking…). |
| Jeton | Authorization: Bearer … | Réservé aux applications Kolia, après connexion. Pas destiné aux intégrations. |
Ne placez jamais une clé dans du code exécuté chez l'utilisateur, navigateur ou application mobile. En cas de fuite, révoquez-la depuis votre espace : elle cesse de fonctionner immédiatement.
Tarifs publics
Deux routes publiques, sans clé API : de quoi afficher un prix indicatif avant qu'un client ait un compte. Elles ne créent aucun devis et n'exposent aucune donnée interne.
La grille
GET /v1/plateforme/tarifs — zones actives et règles de calcul. Limité à 60 requêtes par minute et par adresse IP.
{
"devise": "FCFA",
"zones": [{ "id": "…", "nom": "…", "ville": "…",
"tarifBase": 0, "tarifParKm": 0, "tarifParKg": 0,
"fraisCodPct": 0, "fraisCodFixe": 0 }],
"niveauxDeService": [{ "code": "STANDARD", "multiplicateurPrix": 1, "facteurDelai": 1.8 }],
"delai": { "priseEnChargeMin": 10, "minutesParKm": 4 },
"assurance": { "tauxPct": 1.5, "primeMinimum": 100 },
"devisValiditeMinutes": 15,
"arrondiFcfa": 5,
"regles": [{ "zoneIds": ["…"], "paiementALaLivraisonOuvert": true,
"assuranceOuverte": true,
"majoration": { "active": true, "plages": [ … ],
"surchargeMeteo": { "active": false, "pct": 15 },
"plafondPct": 30 } }]
}
Une plage de majoration porte libelle, jours (0 = dimanche, vide = tous les jours), debut, fin (heure locale), pct et zones (vide = toutes). La commission de la plateforme n'apparaît jamais dans cette réponse.
L'estimation
POST /v1/plateforme/estimation — même calcul que le devis, sur la grille de la zone : computePrice, niveau de service, majoration en vigueur, frais d'encaissement, délai estimé. Limité à 20 requêtes par minute et par adresse IP (429 au-delà).
| Champ | Type | Description |
|---|---|---|
zoneId | texte, requis | Une zone active de GET /v1/plateforme/tarifs. |
distanceKm | nombre, requis | De 0,5 à 50, à vol d'oiseau. |
poidsKg | nombre | De 0 à 100. |
serviceLevel | STANDARD | EXPRESS | PREMIUM | STANDARD par défaut. |
montantCod | entier | Montant à encaisser : ajoute les frais d'encaissement. 403 si le paiement à la livraison est fermé pour cette zone. |
{
"prixCourse": 1210,
"fraisCod": 600,
"majorationPct": 0.1,
"delaiEstimeMin": 34,
"total": 1810,
"estimation": true
}
Une estimation n'est pas un engagement : le devis ferme, valable quinze minutes, se demande avec POST /v1/quotes, sur la distance réelle entre votre point de collecte et le destinataire, remises comprises.
Devis
POST /v1/quotes calcule le prix, les frais et le délai estimé d'une course. Le devis reste valable quinze minutes et ne sert qu'une fois.
| Champ | Type | Description |
|---|---|---|
pickupPointId | texte, requis | Votre point de collecte (liste dans GET /v1/merchants/me). |
destLat, destLng | nombres, requis | Coordonnées du destinataire. |
modePaiement | PREPAYE | COD | Course payée d'avance, ou paiement à la livraison. |
montantCod | entier | Montant à encaisser. Obligatoire en COD. |
serviceLevel | STANDARD | EXPRESS | PREMIUM | STANDARD par défaut. |
poidsKg | nombre | Poids du colis. |
codePromo | texte | Code de remise. |
assurance, valeurDeclaree | booléen, entier | Assurance colis ; la valeur déclarée est alors obligatoire. |
{
"id": "9f0c…",
"distanceKm": 5.84,
"serviceLevel": "EXPRESS",
"prixCourse": 2150,
"fraisCod": 525,
"primeAssurance": 0,
"remise": 0,
"majorationPct": 0,
"delaiEstimeMin": 34,
"valideJusqua": "2026-09-15T14:17:00.000Z"
}
Une origine hors des zones couvertes est refusée (400). Le paiement à la livraison demande une identité validée et un compte de reversement vérifié ; sans cela, l'API répond 403 et le prépayé reste disponible.
Expéditions
POST /v1/shipments crée l'expédition à partir d'un devis valide. Le dispatch démarre aussitôt : aucun autre appel n'est nécessaire.
| Champ | Type | Description |
|---|---|---|
quoteId | texte, requis | Devis valide et non utilisé. |
pickupPointId | texte, requis | Le point de collecte du devis. |
destinataireNom, destinataireTel | textes, requis | Le téléphone reçoit le lien de suivi et le code de remise. |
destAdresse | texte, requis | Adresse lisible par le livreur. |
destRepere | texte | Repère : « face à la pharmacie », « portail bleu »… |
destinataireEmail | texte | Dernier recours si WhatsApp et SMS échouent. |
contenu | texte, requis | Nature du colis. |
instructions | texte | Consignes pour le livreur. |
nombreColis | entier, 1 à 50 | Colis remis au livreur pour cette expédition (1 par défaut). |
creneauDebut, creneauFin | dates ISO 8601 | Livraison planifiée : créneau de 30 min à 12 h, dans les 14 jours. La recherche du livreur démarre à temps pour une remise dans le créneau. |
payVia | MOMO | WALLET | Course prépayée : mobile money (par défaut) ou solde du portefeuille. |
brouillon | booléen | Enregistre sans lancer le dispatch ; à confirmer ensuite. |
Envoyez un en-tête Idempotency-Key (par exemple l'identifiant de la commande dans votre système) : si l'appel est rejoué — coupure réseau, relance automatique —, l'API renvoie l'expédition déjà créée au lieu d'en créer une seconde. Sans cet en-tête, un devis ne sert qu'une fois : un second appel est refusé.
La réponse porte la reference (KOL-2026-XXXXX) et le statut. La page de suivi du destinataire est https://api.kolia.juali.pro/t/{reference}.
Les autres routes
| Route | Rôle |
|---|---|
GET /v1/shipments | Vos expéditions, filtrables par statut, paginées (page, limit). |
GET /v1/shipments/:id | Détail : statut, livreur, chronologie, preuve de livraison. |
PATCH /v1/shipments/:id | Modifier, tant que l'expédition est en brouillon ou en attente. |
POST /v1/shipments/:id/confirm | Confirmer un brouillon et lancer le dispatch. |
POST /v1/shipments/:id/cancel | Annuler. Avant l'enlèvement, le prépayé est rendu. |
POST /v1/shipments/:id/retry | Nouvelle tentative après un échec, dans la limite des tentatives. |
POST /v1/shipments/:id/renvoyer-code | Renvoie le code de remise au téléphone du destinataire, par exemple quand votre client ne l'a pas reçu. Possible une fois le colis récupéré (PRISE_EN_CHARGE, EN_TRANSIT), 3 fois par heure au plus par expédition (429 au-delà). Réponse : { "message": "Code renvoyé au destinataire." }. |
POST /v1/shipments/bulk | Import en masse : jusqu'à 100 lignes, résultat ligne par ligne. Chaque referenceExterne rend la ligne rejouable sans doublon. |
GET /v1/merchants/me/destinataires?q= | Carnet des destinataires déjà livrés, pour pré-remplir une expédition. |
GET /v1/disputes/:id | Dossier d'un litige : échanges, pièces jointes, historique des statuts. |
POST /v1/disputes/:id/messages | Écrire dans le dossier, avec une pièce jointe téléversée (pieceJointeUrl). |
Code de remise
Le code à quatre chiffres est envoyé au destinataire dès que le livreur récupère le colis, et lui seul le connaît : le livreur ne le voit pas, et l'expéditeur non plus.
Les réponses destinées à l'expéditeur — GET /v1/shipments, GET /v1/shipments/:id, création, modification, annulation — ne contiennent plus le code. Le booléen aCodeRemise indique seulement si l'expédition en a un.
Si votre client dit ne pas l'avoir reçu, appelez POST /v1/shipments/:id/renvoyer-code : la plateforme le renvoie au téléphone du destinataire. Le renvoi ne communique jamais le code à l'expéditeur. Il n'est possible qu'en PRISE_EN_CHARGE ou EN_TRANSIT, au plus 3 fois par heure et par expédition.
curl -X POST https://api.kolia.juali.pro/v1/shipments/9f0c…/renvoyer-code \ -H "X-API-Key: $KOLIA_API_KEY" → 200 { "message": "Code renvoyé au destinataire." }
À la remise, cinq essais de saisie sont acceptés ; au-delà, la saisie est bloquée et une alerte est levée.
Bac à sable
Créez une clé de bac à sable dans l'espace commerçant (rubrique Développeurs → type « Bac à sable »), sans rien demander à personne. Elle commence par kolia_tk_ et s'utilise exactement comme une clé réelle, sur les mêmes routes.
- Les expéditions créées avec elle sont isolées : invisibles de l'espace commerçant et de vos clés réelles, et inversement.
- Aucun livreur n'est sollicité, aucun paiement n'est débité, aucun message n'est envoyé au destinataire. Le paiement à la livraison se teste sans vérification d'identité.
- La livraison est simulée : l'expédition passe d'elle-même par tous les statuts jusqu'à
LIVREE, en une minute environ. Vos webhooks reçoivent chaque événement, avec"sandbox": truedansdonnees.
Passez en production en remplaçant la clé kolia_tk_ par une clé kolia_sk_ : rien d'autre ne change.
Statuts
Chaque expédition suit une machine à états stricte. Une transition non prévue est refusée avec 400 et le message Transition interdite.
| Statut | Signification | Événement |
|---|---|---|
BROUILLON | Enregistrée, dispatch non lancé | — |
EN_ATTENTE | Recherche d'un livreur | shipment.created |
AFFECTEE | Offre envoyée à un livreur | shipment.assigned |
ACCEPTEE | Livreur en route vers la collecte | shipment.accepted |
EN_RAMASSAGE | Livreur au point de collecte | shipment.pickup_started |
PRISE_EN_CHARGE | Colis récupéré | shipment.picked_up |
EN_TRANSIT | En route vers le destinataire | shipment.in_transit |
LIVREE | Remis, preuve enregistrée | shipment.delivered |
ECHEC | Remise impossible : absent, refus… | shipment.failed |
ANNULEE | Annulée | shipment.cancelled |
EXPIREE | Aucun livreur trouvé à temps | shipment.expired |
LITIGE | Litige ouvert, instruit par l'équipe | shipment.disputed |
CLOTUREE | Réglée et close | shipment.closed |
Suivi en direct
Chaque expédition a sa page publique, https://api.kolia.juali.pro/t/{reference} : carte en direct, chronologie, notation à la livraison. Pour construire votre propre interface :
GET /v1/tracking/{reference}— l'état public de l'expédition, sans clé, avec l'heure d'arrivée estimée (eta) et le créneau planifié. La même estimation figure dansGET /v1/shipments/:id.- WebSocket (Socket.IO), espace
/tracking— abonnez-vous à une référence, recevezposition,statutetmessage.
import { io } from 'socket.io-client'; const socket = io('https://api.kolia.juali.pro/tracking'); socket.emit('subscribe', { reference: 'KOL-2026-7Q4MX' }); socket.on('position', ({ lat, lng }) => deplacerLivreur(lat, lng)); socket.on('statut', ({ statut }) => afficherStatut(statut));
Webhooks
Souscrivez une URL publique en HTTPS aux événements qui vous intéressent, ou à * pour tout recevoir. La réponse contient le secret de signature whsec_… : conservez-le.
{
"url": "https://votre-boutique.bj/webhooks/kolia",
"events": ["shipment.delivered", "shipment.failed", "payment.settled"]
}
Événements : les douze shipment.* du tableau des statuts, ainsi que payment.settled, payment.refunded et payment.reversed.
Ce que reçoit votre serveur
X-Kolia-Event: shipment.delivered X-Kolia-Delivery: 5d2a… X-Kolia-Signature: sha256=3f9c… { "type": "shipment.delivered", "reference": "KOL-2026-7Q4MX", "donnees": { … }, "horodatage": "2026-09-15T14:26:41.000Z" }
Répondez par un code 2xx en moins de dix secondes. Sinon, la livraison est rejouée après 1 min, 5 min, 30 min puis 2 h, et abandonnée après la cinquième tentative. Le journal de chaque webhook est dans GET /v1/developer/webhooks/:id/deliveries.
Un même événement peut arriver plus d'une fois : dédoublonnez sur X-Kolia-Delivery.
Vérifier la signature
X-Kolia-Signature vaut sha256= suivi du HMAC-SHA256 du corps brut, calculé avec votre secret. Comparez en temps constant, avant tout traitement.
import express from 'express'; import { verifyWebhookSignature } from 'kolia-sdk'; app.post('/webhooks/kolia', express.raw({ type: 'application/json' }), (req, res) => { const brut = req.body.toString(); if (!verifyWebhookSignature(process.env.KOLIA_WEBHOOK_SECRET, brut, req.get('X-Kolia-Signature'))) { return res.status(401).end(); } const evenement = JSON.parse(brut); // evenement.type, evenement.reference, evenement.donnees res.status(200).end(); });
$brut = file_get_contents('php://input'); $attendu = 'sha256=' . hash_hmac('sha256', $brut, getenv('KOLIA_WEBHOOK_SECRET')); if (!hash_equals($attendu, $_SERVER['HTTP_X_KOLIA_SIGNATURE'] ?? '')) { http_response_code(401); exit; } $evenement = json_decode($brut, true); http_response_code(200);
Erreurs
Toutes les erreurs ont la même forme, avec un message en français prêt à afficher. details n'apparaît que pour les erreurs de validation.
{
"code": "BAD_REQUEST",
"message": "Requête invalide",
"details": ["destLat must be a number …"]
}
| HTTP | Cas | Que faire |
|---|---|---|
400 | Corps invalide ou champ inconnu, devis expiré ou déjà utilisé, transition interdite. | Corriger la requête ; relire le statut courant. |
401 | Clé ou jeton absent, invalide ou révoqué. | Vérifier l'en-tête d'authentification. |
403 | Action non autorisée : paiement à la livraison non débloqué, portée de clé insuffisante. | Le message indique la condition à remplir. |
404 | Ressource introuvable, ou hors de votre compte. | Vérifier l'identifiant. |
429 | Débit ou quota dépassé. | Réessayer plus tard, avec un délai croissant. |
5xx | Erreur du serveur. | Réessayer ; consulter la page de statut. |
SDK
Deux clients officiels, JavaScript (Node 18 et plus, zéro dépendance) et PHP (une classe, cURL natif), exposent les mêmes méthodes.
| Méthode | Route |
|---|---|
createQuote(params) | POST /v1/quotes |
createShipment(params) | POST /v1/shipments |
getShipment(id) | GET /v1/shipments/:id |
cancelShipment(id) | POST /v1/shipments/:id/cancel |
resendDeliveryCode(id) | POST /v1/shipments/:id/renvoyer-code |
listShipments() | GET /v1/shipments |
track(reference) | GET /v1/tracking/:reference |
getFinances() | GET /v1/merchants/me/finances |
listPickupPoints() | GET /v1/merchants/me |
verifyWebhookSignature(secret, brut, signature) | Vérification locale, en temps constant. |
Les SDK ne sont pas publiés sur npm ni sur Packagist : ils vous sont remis, avec l'extension WooCommerce, sur demande à hello@juali.pro, objet « Développeurs ».