Guide développeur
Tout pour intégrer NabilaPay : authentification, idempotence, webhooks et mode test.
Démarrage rapide
Trois étapes suffisent pour votre premier paiement de test.
- Créez un compte et confirmez votre e-mail avec le code reçu.
- Connectez-vous : trois clés sont générées automatiquement : publique (
nabila_pk_test_…), secrète (nabila_sk_test_…) et privée (nabila_prv_test_…). Copiez la clé privée : elle ne sera plus affichée. - Envoyez votre première requête :
curl -X POST "http://nabila.bconsultingsarl.com//api/v1/payments" \
-H "Authorization: Bearer $NABILA_KEY" \
-H "Idempotency-Key: commande-1001" \
-H "Content-Type: application/json" \
-d '{"amount":15000,"currency":"XOF","country":"BJ","phone":"+22961000000","description":"Commande 1001","external_ref":"CMD-1001","metadata":{"panier":"A-77"}}'<?php
$ch = curl_init('http://nabila.bconsultingsarl.com//api/v1/payments');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NABILA_KEY'),
'Idempotency-Key: commande-1001',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 15000,
'currency' => 'XOF',
'country' => 'BJ',
'phone' => '+22961000000',
'description' => 'Commande 1001',
'external_ref' => 'CMD-1001',
'metadata' => [
'panier' => 'A-77',
],
]),
]);
$reponse = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
var_dump($code, $reponse);const res = await fetch('http://nabila.bconsultingsarl.com//api/v1/payments', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NABILA_KEY}`,
'Idempotency-Key': 'commande-1001',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"amount": 15000,
"currency": "XOF",
"country": "BJ",
"phone": "+22961000000",
"description": "Commande 1001",
"external_ref": "CMD-1001",
"metadata": {
"panier": "A-77"
}
}),
});
console.log(res.status, await res.json());import os, requests
r = requests.request(
'POST', 'http://nabila.bconsultingsarl.com//api/v1/payments',
headers={
'Authorization': 'Bearer ' + os.environ['NABILA_KEY'],
'Idempotency-Key': 'commande-1001',
},
json={"amount":15000,"currency":"XOF","country":"BJ","phone":"+22961000000","description":"Commande 1001","external_ref":"CMD-1001","metadata":{"panier":"A-77"}},
timeout=10,
)
print(r.status_code, r.json())L'URL de base de l'API est http://nabila.bconsultingsarl.com/api/v1. Toutes les réponses sont en JSON UTF-8. Les montants sont des entiers en FCFA (XOF) : 15000 signifie 15 000 FCFA.
Authentification
Envoyez votre clé dans l'en-tête Authorization: Bearer nabila_prv_test_…. Chaque compte possède trois clés : la clé privée (nabila_prv_…) authentifie vos appels serveur ; la clé publique (nabila_pk_…) est en lecture seule et peut être exposée côté client ; la clé secrète (nabila_sk_…) signe les webhooks et ne sert jamais à l'authentification. Les clés Sandbox (_test_) et Production (_live_) sont strictement séparées : une clé de test est refusée en Production et inversement. Seule l'empreinte (SHA-256) de la clé est conservée : en cas de perte, renouvelez-la depuis votre espace.
Ne mettez jamais une clé dans une application mobile, un site public ou un dépôt Git. Gardez-la côté serveur, dans une variable d'environnement.
Idempotence
Les requêtes d'écriture (paiement, remboursement, payout) exigent l'en-tête Idempotency-Key (8 à 64 caractères). Si votre réseau coupe et que vous renvoyez la même requête avec la même clé, vous récupérez le même objet (en-tête de réponse Idempotent-Replayed: true) : aucun double débit. Réutiliser une clé avec un montant différent est refusé.
Statuts d'un paiement
| Statut | Signification |
|---|---|
succeeded | Le client a payé, votre solde est crédité du net. |
failed | Refusé ou rejeté. Voir failure_reason. |
pending | Réponse ambiguë ou en attente du réseau. Ne renvoyez pas le paiement : attendez le webhook ou consultez-le. |
partially_refunded / refunded | Remboursé en partie / en totalité. |
Erreurs
Une erreur renvoie un code HTTP et un objet error stable.
HTTP/1.1 400
{
"error": {
"type": "invalid_request",
"code": "out_of_range",
"message": "« amount » doit être compris entre 100 et 1000000.",
"param": "amount",
"request_id": "req_5d1c0a9f3b2e7a41"
}
}| HTTP | Type | Quand |
|---|---|---|
| 400 | invalid_request | Paramètre manquant ou invalide, idempotence absente. |
| 401 | authentication_error | Clé absente, inconnue, révoquée ou expirée. |
| 403 | permission_error | Droit manquant, IP non autorisée, Production indisponible. |
| 404 | not_found | Ressource inconnue ou appartenant à un autre compte. |
| 422 | request_rejected | Règle métier : solde insuffisant, remboursement trop élevé… |
| 429 | rate_limit_error | Trop de requêtes. |
| 5xx | api_error | Erreur chez NabilaPay. Communiquez request_id. |
Limites
60 requêtes par minute et par clé. Les en-têtes X-RateLimit-Limit et X-RateLimit-Remaining sont renvoyés ; au-delà, la réponse 429 indique Retry-After. Corps de requête : 64 Ko maximum.
Webhooks
Déclarez une adresse dans votre espace (ou via POST /webhooks). NabilaPay y envoie un événement JSON à chaque changement d'état : payment.succeeded, payment.failed, payment.pending, refund.created, payout.created, payout.succeeded, payout.failed. Répondez 2xx dans les 5 secondes ; sinon la livraison est retentée (jusqu'à 5 fois) et reste visible dans votre journal.
Chaque requête porte Nabila-Signature: t=<horodatage>,v1=<hmac>. Vérifiez-la avec le secret affiché à la création du webhook, sur le corps brut :
<?php
$corps = file_get_contents('php://input');
[$t, $v1] = array_map(fn($p) => explode('=', $p, 2)[1], explode(',', $_SERVER['HTTP_NABILA_SIGNATURE']));
$attendu = hash_hmac('sha256', $t . '.' . $corps, getenv('NABILA_WEBHOOK_SECRET'));
if (!hash_equals($attendu, $v1) || abs(time() - (int)$t) > 300) { http_response_code(400); exit; }
$evenement = json_decode($corps, true);
http_response_code(200);import crypto from 'node:crypto';
app.post('/webhooks/nabila', express.raw({ type: 'application/json' }), (req, res) => {
const [t, v1] = req.get('Nabila-Signature').split(',').map(p => p.split('=')[1]);
const attendu = crypto.createHmac('sha256', process.env.NABILA_WEBHOOK_SECRET).update(t + '.' + req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(v1)) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
const evenement = JSON.parse(req.body);
res.sendStatus(200);
});import hmac, hashlib, os, time
def verifier(corps: bytes, entete: str) -> bool:
t, v1 = [p.split('=', 1)[1] for p in entete.split(',')]
attendu = hmac.new(os.environ['NABILA_WEBHOOK_SECRET'].encode(), f'{t}.'.encode() + corps, hashlib.sha256).hexdigest()
return hmac.compare_digest(attendu, v1) and abs(time.time() - int(t)) < 300Mode test et mode réel
Tous les paiements sont réglés par KKiaPay. En mode test, les paiements utilisent le bac à sable KKiaPay (argent fictif) ; en mode réel, ils débitent de vrais comptes Mobile Money ou cartes. Vos clés nabila_test_ restent en test, vos clés nabila_live_ passent en réel après validation KYB.
Passer en production
La Production s'ouvre après validation KYB (pièces envoyées dans votre espace) Tant qu'elle n'est pas ouverte sur votre compte, les clés nabila_live_ sont refusées avec le code production_unavailable : c'est volontaire : aucun paiement réel n'est ouvert avant la validation de votre dossier.
Testez votre intégration aujourd'hui.
Compte gratuit, clé API Sandbox générée automatiquement, aucun argent réel.