API marchands e-Wari

Version 1 · serveur à serveur · JSON · HTTPS. Environnement pilote : clés ewk_test_…, aucune valeur légale.

Principe

  1. Votre serveur crée un paiement : e-Wari renvoie un checkout_url.
  2. Vous redirigez le client vers ce lien : il paie dans l'appli e-Wari (QR code ou bouton sur téléphone) et confirme avec son code PIN.
  3. e-Wari envoie un webhook signé payment.succeeded à votre serveur, puis redirige le client vers votre return_url.
  4. Ne livrez la commande qu'après le webhook (ou un GET qui confirme status = succeeded) : le retour du navigateur seul ne prouve rien.

Base : https://marchands-pilote.up.railway.app/v1 · Authentification : Authorization: Bearer ewk_test_… (clé créée dans l'espace marchand). Montants en F CFA entiers (XOF), de 100 à 2 000 000. 120 requêtes par minute au plus.

Les quatre appels

AppelRôle
POST /v1/paymentsCréer un paiement. Champs : amount (obligatoire), reference (n° de commande, obligatoire), currency (XOF), description, customer_phone, return_url, cancel_url, metadata (objet), expires_in (300 à 86 400 s, 1 800 par défaut). En-tête conseillé : Idempotency-Key. Réponse 201.
GET /v1/payments/{id}Consulter un paiement et son statut à jour.
POST /v1/payments/{id}/cancelAnnuler un paiement encore en attente.
POST /v1/payments/{id}/refundDemander le remboursement total d'un paiement réussi (reason facultatif). Réponse 202 : le service client e-Wari valide, puis le webhook payment.refunded part.

Bac à sable : POST /v1/payments/{id}/test_complete simule le paiement du client (clés test uniquement).

Exemple

curl https://marchands-pilote.up.railway.app/v1/payments \
  -H "Authorization: Bearer ewk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" -H "Idempotency-Key: commande-1042" \
  -d '{"amount":25000,"reference":"CMD-1042","description":"Frais de dossier","return_url":"https://votre-site.com/paiement/retour"}'
{ "id": "pay_7KQ2M9XW4D1P8R3T", "object": "payment", "status": "pending", "amount": 25000, "currency": "XOF",
  "reference": "CMD-1042", "checkout_url": "https://marchands-pilote.up.railway.app/c/pay_7KQ2M9XW4D1P8R3T", "expires_at": "…", "livemode": false, … }

Statuts : pending, succeeded, failed, cancelled, expired, refunded. Un remboursement en cours apparaît dans refund.status (pending, succeeded, rejected). Erreurs : {"error":{"code":"…","message":"…","param":"…"}} avec les codes HTTP 400, 401, 404, 409, 429.

Webhooks signés

Événements : payment.succeeded, payment.failed, payment.cancelled, payment.expired, payment.refund_requested, payment.refunded, payment.refund_rejected (et ping pour les tests). Corps : {"id":"evt_…","type":"…","created_at":"…","data":{"payment":{…}}}.

En-tête EWari-Signature: t=<horodatage>,v1=<signature> où signature = HMAC-SHA256 en hexadécimal, avec votre secret whsec_…, de la chaîne t + "." + corps brut. Vérifiez la signature, refusez un horodatage de plus de 5 minutes, et traitez chaque id d'événement une seule fois. Répondez 2xx en moins de 10 s ; sinon e-Wari réessaie jusqu'à 10 fois sur 24 h.

// Node.js (Express : express.raw({ type: "application/json" }))
const crypto = require("crypto");
function verifier(corpsBrut, entete, secret) {
  const p = Object.fromEntries(entete.split(",").map(x => x.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const attendu = crypto.createHmac("sha256", secret).update(p.t + "." + corpsBrut).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(p.v1 || ""));
}
// PHP
function verifier($corpsBrut, $entete, $secret) {
  parse_str(str_replace(",", "&", $entete), $p);
  if (abs(time() - (int)$p["t"]) > 300) return false;
  return hash_equals(hash_hmac("sha256", $p["t"] . "." . $corpsBrut, $secret), $p["v1"] ?? "");
}