Intégrer NaabigaPay dans Flutter
Accepter des paiements NaabigaPay dans votre application, en trois étapes. Rien d'autre.
sk_live_…) ne doit jamais être mise dans l'app Flutter : un APK se décompile et la clé fuiterait. L'app parle à votre serveur ; c'est votre serveur qui appelle NaabigaPay avec la clé.Le principe
App Flutter ─► VOTRE serveur (détient sk_live_…) ─► API NaabigaPay /v1
▲ ▲
│ ouvre l'URL │ reçoit le webhook signé « payment.succeeded »
│ de paiement │ et marque la commande comme payée- L'app demande un lien de paiement à votre serveur.
- L'app ouvre l'URL renvoyée (page de paiement NaabigaPay).
- Le paiement confirmé, NaabigaPay appelle le webhook de votre serveur ; l'app relit le statut chez vous.
1.Créer le lien (côté serveur)
Votre serveur appelle POST/v1/payment-links avec la clé secrète. Exemple en Dart (le même principe en Node, PHP, Python…) :
import 'dart:convert';
import 'package:http/http.dart' as http;
class NaabigaPay {
NaabigaPay(this.secret); // sk_live_... — RESTE SUR LE SERVEUR
final String secret;
final String base = 'https://api.pay.naabiga.com/api/v1';
Map<String, String> get _h => {
'Authorization': 'Bearer $secret',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
/// Crée un lien de paiement (montant en FCFA). Renvoie { token, url, ... }.
Future<Map<String, dynamic>> createPaymentLink({
required int amount,
String? reference, // votre numéro de commande
String? description,
String? returnUrl, // deep link de retour vers l'app
String? cancelUrl,
String? idempotencyKey, // rejeu réseau sans double-création
}) async {
final res = await http.post(
Uri.parse('$base/payment-links'),
headers: { ..._h, if (idempotencyKey != null) 'Idempotency-Key': idempotencyKey },
body: jsonEncode({
'amount': amount,
if (reference != null) 'reference': reference,
if (description != null) 'description': description,
if (returnUrl != null) 'return_url': returnUrl,
if (cancelUrl != null) 'cancel_url': cancelUrl,
'single_use': true,
}),
);
if (res.statusCode != 201) {
throw Exception('NaabigaPay ${res.statusCode}: ${res.body}');
}
return jsonDecode(res.body) as Map<String, dynamic>;
}
/// Statut d'un paiement : pending | success | failed | cancelled.
Future<Map<String, dynamic>> getPayment(String reference) async {
final res = await http.get(Uri.parse('$base/payments/$reference'), headers: _h);
if (res.statusCode != 200) {
throw Exception('NaabigaPay ${res.statusCode}: ${res.body}');
}
return jsonDecode(res.body) as Map<String, dynamic>;
}
}La réponse contient url (ex. https://pay.naabiga.com/link/9f3c…) : c'est la page où le client paie. Pour un montant libre, n'envoyez pas amount mais une description (bornes optionnelles amount_min / amount_max).
2.Ouvrir le paiement dans l'app
L'app demande le lien à votre serveur, puis ouvre l'url avec url_launcher :
import 'package:url_launcher/url_launcher.dart';
Future<void> payer(String commandeId) async {
// 1) Votre serveur crée le lien (il a la clé) et renvoie l'URL de paiement.
final data = await monBackend.creerLien(
commande: commandeId, // -> reference NaabigaPay
returnUrl: 'monapp://paiement/retour', // deep link de votre app
cancelUrl: 'monapp://paiement/annule',
);
// 2) Ouvrir la page de paiement NaabigaPay (Mobile money / solde).
await launchUrl(Uri.parse(data['url']), mode: LaunchMode.externalApplication);
}Astuce. LaunchMode.externalApplication ouvre le navigateur système (recommandé pour le mobile money). Vous pouvez aussi utiliser une WebView in-app et intercepter la navigation vers votre return_url.
3.Confirmer le paiement
Deux signaux, un seul fait foi :
- Le retour dans l'app (deep link
monapp://paiement/retour) : pratique pour rafraîchir l'écran, mais ne garantit pas le paiement. - Le webhook signé reçu par votre serveur : c'est la source de vérité. Votre serveur marque la commande payée, l'app relit le statut chez vous.
Capter le retour avec app_links puis vérifier auprès de votre serveur :
import 'package:app_links/app_links.dart';
final _appLinks = AppLinks();
void ecouterRetourPaiement() {
_appLinks.uriLinkStream.listen((uri) async {
if (uri.scheme == 'monapp' && uri.host == 'paiement') {
// Demander le statut à VOTRE serveur (qui a reçu le webhook signé).
final statut = await monBackend.statutCommande(/* commandeId */);
// statut == 'success' -> afficher « Paiement réussi »
}
});
}Webhook (rappel côté serveur)
NaabigaPay envoie POST à l'URL enregistrée dans le portail marchand, avec :
| En-tête | Valeur |
|---|---|
X-NPay-Event | payment.succeeded |
X-NPay-Signature | HMAC-SHA256 du corps brut, signé avec le secret du webhook |
Recalculez le HMAC-SHA256 sur le corps brut avec le secret du webhook, comparez à X-NPay-Signature, et n'acceptez le paiement que si la signature correspond.
Configurer le deep link monapp://
Android — dans android/app/src/main/AndroidManifest.xml, à l'intérieur de l'activité :
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="monapp" android:host="paiement" />
</intent-filter>iOS — dans ios/Runner/Info.plist :
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>monapp</string></array>
</dict>
</array>Sandbox et passage en production
- Créez une clé en mode sandbox (
sk_sandbox_…) pour tester sans argent réel. - Basculez sur
sk_live_…en production, uniquement côté serveur. - Ajoutez un
Idempotency-Keypar tentative pour éviter tout double-lien en cas de retry réseau.
Référence rapide
| Méthode | Endpoint | Rôle |
|---|---|---|
| POST | /v1/payment-links | Créer un lien de paiement |
| GET | /v1/payment-links/{token} | Détail d'un lien |
| GET | /v1/payments/{reference} | Statut d'un paiement |
| GET | /v1/transactions | Paiements reçus (paginé) |
Base : https://api.pay.naabiga.com/api/v1 · Auth : Authorization: Bearer sk_…. La documentation complète détaille chaque endpoint et le format des webhooks.