Naɓanam

Documentation de l'API

Estimez le prix d'une course, publiez-la, suivez le livreur et recevez chaque statut dans votre application.

URL de base https://api.nabanam.educaly.net/api/v1

Démarrer

  1. Demandez un accès partenaire. Naɓanam crée votre compte et définit avec vous votre mode d'encaissement.
  2. Connectez-vous à l'espace partenaire et créez une clé d'API.
  3. Déclarez l'adresse de votre webhook et abonnez-la à delivery.status_changed.
  4. Estimez un prix, puis publiez votre première course.

Authentification

Envoyez votre clé dans l'en-tête x-api-key de chaque requête. Les corps de requête et de réponse sont en JSON.

Exemple

curl https://api.nabanam.educaly.net/api/v1/deliveries/calculate-price \
  -H "x-api-key: nbn_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "pickupPosition": { "lat": 14.6937, "lng": -17.4441 }, "dropoffPosition": { "lat": 14.7167, "lng": -17.4677 } }'

Appelez l'API depuis votre serveur uniquement. Une clé placée dans une application mobile ou un site web peut être lue par n'importe qui. En cas de doute, révoquez-la dans l'espace partenaire et créez-en une autre.

Estimer le prix

POST/deliveries/calculate-price

Renvoie le prix d'une course entre deux points, sans la publier. Affichez-le à votre client avant le paiement.

Corps de la requête

pickupPositionobjet { lat, lng }obligatoire
Coordonnées GPS du point de retrait, en nombres décimaux.
dropoffPositionobjet { lat, lng }obligatoire
Coordonnées GPS du point de livraison.

Réponse 201

distanceMnombre
Distance estimée du trajet, en mètres. Elle est corrigée pour approcher le trajet par la route.
priceentier
Prix de la course en FCFA, à afficher à votre client.

Requête

POST /api/v1/deliveries/calculate-price

{
  "pickupPosition":  { "lat": 14.6937, "lng": -17.4441 },
  "dropoffPosition": { "lat": 14.7167, "lng": -17.4677 }
}

Réponse

{
  "distanceM": <distance en mètres>,
  "price":     <prix en FCFA>
}

Le prix dépend de la distance, selon une grille par tranches. Le même calcul s'applique à la publication.

Publier une course

POST/deliveries

Publiez la course quand votre client a validé sa commande. Elle est ensuite proposée aux livreurs proches du point de retrait.

Corps de la requête

externalReftexte obligatoire
Votre référence de commande. Elle est unique pour votre compte : une référence déjà utilisée est refusée.
pickupAddresstexte obligatoire
Adresse du point de retrait, telle que le livreur la lira.
pickupPositionobjet { lat, lng } obligatoire
Coordonnées GPS du point de retrait.
dropoffAddresstexte obligatoire
Adresse de livraison.
dropoffPositionobjet { lat, lng } obligatoire
Coordonnées GPS du point de livraison.
senderNametexte obligatoire
Nom du vendeur ou du point de retrait.
senderPhonetexte obligatoire
Téléphone du vendeur. Le livreur peut l'appeler depuis l'application.
recipientNametexte obligatoire
Nom du destinataire.
recipientPhonetexte obligatoire
Téléphone du destinataire. Format international conseillé, par exemple +221….
packageDescriptiontexte facultatif
Description du colis, visible par le livreur.
successUrlURL obligatoire en encaissement Wave
Page où votre client revient après un paiement réussi.
errorUrlURL obligatoire en encaissement Wave
Page où votre client revient si le paiement échoue.

Requête

POST /api/v1/deliveries

{
  "externalRef": "CMD-1042",
  "pickupAddress": "Rue 10, Point E, Dakar",
  "pickupPosition": { "lat": 14.6937, "lng": -17.4441 },
  "dropoffAddress": "Cité Keur Gorgui, Dakar",
  "dropoffPosition": { "lat": 14.7167, "lng": -17.4677 },
  "senderName": "Boutique Point E",
  "senderPhone": "+221 77 000 00 01",
  "recipientName": "Awa Ndiaye",
  "recipientPhone": "+221 77 000 00 02",
  "packageDescription": "Deux plats, un sac",
  "successUrl": "https://votre-app.sn/commandes/CMD-1042/paiement-reussi",
  "errorUrl": "https://votre-app.sn/commandes/CMD-1042/paiement-echoue"
}

Réponse 201, encaissement par vous

La course est publiée immédiatement. Conservez son id pour le suivi.

{
  "id": "<identifiant de la course>",
  "externalRef": "CMD-1042",
  "status": "PENDING",
  "paymentStatus": "CONFIRMED",
  "price": <prix en FCFA>,
  "createdAt": "…"
}

Réponse 201, encaissement par Wave

Redirigez votre client vers waveLaunchUrl. Il paie le prix de la course plus 1 % de frais Wave, arrondi aux 5 FCFA supérieurs. Après un paiement réussi, il revient sur successUrl et la course est publiée. Sinon, il revient sur errorUrl et la course n'est pas proposée aux livreurs.

{
  "waveLaunchUrl": "<lien de paiement Wave>",
  "clientReference": "<référence du paiement>"
}

Cette réponse ne contient pas l'identifiant de la course : suivez-la avec votre externalRef.

Suivre une course

GET/deliveries/:id/tracking

GET/deliveries/by-ref/:externalRef/tracking

Renvoie le statut de la course, le livreur assigné, sa dernière position et son heure d'arrivée estimée. Utilisez l'identifiant de la course ou votre référence.

Réponse 200

statustexte
Statut de la course. Voir Statuts d'une course.
delivererobjet ou null
Livreur assigné : fullName, phone, imageUrl, vehicleType (BIKE, SCOOTER ou TRICYCLE), vehiclePlateNumber. Vaut null tant qu'aucun livreur n'a accepté la course.
positionobjet ou null
Dernière position connue du livreur : lat, lng, updatedAt.
etaobjet ou null
Arrivée estimée : target vaut PICKUP avant le retrait et DROPOFF ensuite, avec distanceM, durationSec et estimatedArrivalAt. Vaut null sans position du livreur ou hors trajet.
GET /api/v1/deliveries/by-ref/CMD-1042/tracking

{
  "deliveryId": "<identifiant de la course>",
  "externalRef": "CMD-1042",
  "status": "ACCEPTED",
  "deliverer": {
    "fullName": "…",
    "phone": "…",
    "imageUrl": "…",
    "vehicleType": "SCOOTER",
    "vehiclePlateNumber": "…"
  },
  "position": { "lat": 14.7012, "lng": -17.4498, "updatedAt": "…" },
  "eta": {
    "target": "PICKUP",
    "distanceM": <distance restante en mètres>,
    "durationSec": <durée estimée en secondes>,
    "estimatedArrivalAt": "…"
  }
}

L'acceptation d'une course n'est pas encore envoyée par webhook. Pour savoir qu'un livreur a pris la course, consultez cet endpoint : deliverer n'est plus null et status vaut ACCEPTED.

Webhooks

Naɓanam envoie une requête POST à votre adresse à chaque changement de statut d'une course. Déclarez l'adresse dans l'espace partenaire : elle doit être en HTTPS et publique. Les adresses locales ou privées sont refusées.

Requête reçue par votre serveur

POST https://votre-app.sn/webhooks/nabanam
Content-Type: application/json
X-Nabanam-Signature: sha256=<signature>

{
  "event": "delivery.status_changed",
  "payload": {
    "externalRef": "CMD-1042",
    "deliveryId": "<identifiant de la course>",
    "previousStatus": "ACCEPTED",
    "newStatus": "PENDING",
    "occurredAt": "…",
    "cancellation": { "reason": "BREAKDOWN", "label": "Panne" }
  }
}

Quand un événement est envoyé

  • Le livreur récupère le colis (PICKED_UP), part vers le destinataire (IN_PROGRESS) ou livre (DELIVERED).
  • La course revient à PENDING : le livreur l'a rendue, et cancellation donne le motif, ou il ne l'a pas récupérée à temps, sans cancellation. Elle est de nouveau proposée aux livreurs.
  • L'équipe Naɓanam modifie un statut, par exemple pour annuler une course. Le motif, s'il est saisi, est dans cancellation.

Motifs dans cancellation.reason

BREAKDOWN
Panne
UNFORESEEN
Imprévu
PRICE_TOO_LOW
Prix trop bas
AREA_INACCESSIBLE
Zone inaccessible
MERCHANT_UNREACHABLE
Vendeur injoignable
OTHER
Autre
ADMIN
Motif saisi par l'équipe Naɓanam

Le champ label contient le motif en clair, prêt à afficher.

Vérifier la signature

L'en-tête X-Nabanam-Signature vaut sha256= suivi du HMAC-SHA256 du corps brut, calculé avec le secret de votre webhook. Le secret est visible dans l'espace partenaire. Refusez toute requête dont la signature ne correspond pas.

Node.js et Express

import crypto from "node:crypto";
import express from "express";

const app = express();

// La signature porte sur le corps brut : lisez-le avant de le parser.
app.post("/webhooks/nabanam", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.NABANAM_WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");
  const received = req.get("X-Nabanam-Signature") ?? "";

  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!valid) return res.sendStatus(401);

  const { event, payload } = JSON.parse(req.body);
  // Mettez à jour la commande payload.externalRef avec payload.newStatus
  res.sendStatus(200);
});

Réponses et nouvelles tentatives

  • Répondez par un code 2xx rapidement, puis traitez l'événement.
  • Sans réponse 2xx, l'envoi est retenté, jusqu'à 5 tentatives au total, avec un délai qui double à partir de 2 secondes.
  • Un même événement peut donc arriver plus d'une fois, et dans le désordre. Utilisez occurredAt pour ignorer un statut plus ancien que celui que vous avez déjà.

Tester votre adresse

Dans l'espace partenaire, le bouton de test envoie deux requêtes avec l'événement ping et des données fictives : la première avec une signature invalide, à laquelle votre serveur doit répondre 401, la seconde avec une signature valide, qui doit recevoir un 2xx. Le journal des envois montre le code renvoyé par votre serveur.

Statuts d'une course

PENDING
En attente d'un livreur. La course est proposée aux livreurs proches. En encaissement Wave, elle ne l'est qu'après confirmation du paiement.
ACCEPTED
Un livreur a accepté la course et se rend au point de retrait.
PICKED_UP
Colis récupéré. Le livreur l'a confirmé à moins de 100 mètres du point de retrait.
IN_PROGRESS
En route vers le destinataire. Étape facultative : une course peut passer directement de PICKED_UP à DELIVERED.
DELIVERED
Livré. Le livreur l'a confirmé à moins de 100 mètres du point de livraison.
CANCELLED
Course annulée, par l'équipe Naɓanam ou après un paiement Wave qui n'a pas abouti.

Paiement

Le mode d'encaissement est défini à l'ouverture de votre compte. Dans les deux cas, Naɓanam paie le livreur par Wave après la livraison.

Encaissement par Wave

Naɓanam crée un lien de paiement Wave à la publication. Votre client paie, puis la course part vers les livreurs. successUrl et errorUrl sont obligatoires.

Encaissement par vous

Vous collectez les frais de livraison dans votre propre parcours. La course est publiée dès l'appel à l'API.

Erreurs

Une erreur renvoie un code HTTP et un corps JSON. message est un texte, ou une liste pour les erreurs de validation.

{
  "statusCode": 400,
  "message": ["recipientPhone should not be empty"],
  "error": "Bad Request"
}
400Bad Request
Champ manquant ou mal formé (message liste les champs concernés), référence déjà utilisée, ou successUrl et errorUrl absents en encaissement Wave.
401Unauthorized
Clé d'API absente, invalide ou révoquée.
404Not Found
Course introuvable pour votre compte, ou aucune tranche tarifaire ne couvre cette distance.
5xxErreur serveur
Erreur de notre côté, par exemple un lien de paiement Wave qui n'a pas pu être créé. La référence a pu être enregistrée : réessayez avec une nouvelle externalRef.

Une question sur l'intégration ? Écrivez-nous.