Accept payments from Wave, Orange Money, PayDunya, PayTech, Stripe & PayPal in your Laravel app with a single, clean API.
Les developpeurs en Afrique de l'Ouest integrent manuellement chaque passerelle de paiement dans chaque projet. Wave, Orange Money, PayDunya, PayTech... chacun avec son API, ses webhooks, ses signatures.
AfriPay unifie tout ca en une seule interface :
// Payer via Wave
$payment = AfriPay::via('wave')->charge([
'amount' => 15000,
'currency' => 'XOF',
'description' => 'Abonnement Premium',
'success_url' => route('payment.success'),
'error_url' => route('payment.error'),
]);
return redirect($payment['redirect_url']);Changer de passerelle ? Une seule ligne :
AfriPay::via('stripe')->charge([...]);
AfriPay::via('paydunya')->charge([...]);
AfriPay::via('orange_money')->charge([...]);| Passerelle | Pays | Type | Statut |
|---|---|---|---|
| Wave | SN, CI, ML, BF | Mobile Money | Production |
| Orange Money | SN, CI, ML, BF, CM, GN | Mobile Money | Beta |
| PayDunya | SN, CI, BJ, TG, BF, ML | Multi-canal | Production |
| PayTech | SN | Multi-canal | Production |
| Stripe | Global | Carte bancaire | Production |
| PayPal | Global | International | Production |
composer require sunucode/afripayPublier la configuration :
php artisan vendor:publish --tag=afripay-configLancer les migrations :
php artisan migrateAjoutez vos cles dans .env :
# Passerelle par defaut
AFRIPAY_DEFAULT_GATEWAY=wave
AFRIPAY_CURRENCY=XOF
# Securite : seuls les webhooks peuvent confirmer un paiement (recommande)
# Mettre a false en dev si les webhooks ne peuvent pas atteindre votre serveur
AFRIPAY_TRUST_WEBHOOK_ONLY=true
# Activer/desactiver les passerelles individuellement
AFRIPAY_WAVE_ENABLED=true
AFRIPAY_STRIPE_ENABLED=true
AFRIPAY_PAYDUNYA_ENABLED=true
AFRIPAY_PAYTECH_ENABLED=true
AFRIPAY_ORANGE_MONEY_ENABLED=false
AFRIPAY_PAYPAL_ENABLED=false
# Wave
WAVE_API_KEY=wave_sn_...
WAVE_API_SECRET=wave_sn_...
WAVE_WEBHOOK_SECRET=wave_sn_WHS_...
# Stripe
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# PayDunya
PAYDUNYA_MASTER_KEY=...
PAYDUNYA_PRIVATE_KEY=...
PAYDUNYA_TOKEN=...
PAYDUNYA_MODE=test
# Orange Money
ORANGE_MONEY_CLIENT_ID=...
ORANGE_MONEY_CLIENT_SECRET=...
ORANGE_MONEY_MERCHANT_KEY=...
# PayPal
PAYPAL_CLIENT_ID=...
PAYPAL_CLIENT_SECRET=...
PAYPAL_MODE=sandbox
# PayTech
PAYTECH_API_KEY=...
PAYTECH_API_SECRET=...
PAYTECH_ENV=testuse SunuCode\AfriPay\Facades\AfriPay;
$payment = AfriPay::via('wave')->charge([
'amount' => 25000,
'currency' => 'XOF',
'description' => 'Commande #1234',
'success_url' => route('orders.payment.success'),
'error_url' => route('orders.payment.error'),
'metadata' => [
'order_id' => 1234,
'user_id' => auth()->id(),
],
]);
// $payment['redirect_url'] -> URL de paiement (rediriger l'utilisateur)
// $payment['transaction'] -> Instance Transaction (sauvegardee en DB)
return redirect($payment['redirect_url']);$payment = AfriPay::via('paydunya')->charge([
'amount' => 9900,
'success_url' => route('subscription.success'),
'error_url' => route('subscription.error'),
'payable_type' => Subscription::class,
'payable_id' => $subscription->id,
]);// app/Providers/EventServiceProvider.php
// ou dans un Listener dedie
use SunuCode\AfriPay\Events\PaymentCompleted;
use SunuCode\AfriPay\Events\PaymentFailed;
use SunuCode\AfriPay\Events\PaymentRefunded;
class HandlePaymentCompleted
{
public function handle(PaymentCompleted $event): void
{
$transaction = $event->transaction;
// Activer l'abonnement, envoyer un email, etc.
$subscription = $transaction->payable;
$subscription->activate();
// Acceder aux metadonnees
$orderId = $transaction->metadata['order_id'] ?? null;
}
}Si le webhook n'est pas encore arrive quand l'utilisateur revient :
// Route de succes
public function paymentSuccess(Request $request)
{
$transaction = Transaction::where('reference', $request->reference)->first();
if ($transaction->status->isPending()) {
// Verifier aupres de la passerelle ET dispatcher l'event si confirme
$transaction = AfriPay::verifyAndProcess($transaction);
}
if ($transaction->status->isCompleted()) {
return view('payment.success');
}
return view('payment.pending');
}$transaction = AfriPay::refund($transaction, 'Client insatisfait');
// Dispatche PaymentRefunded// Toutes les passerelles activees via .env
$gateways = AfriPay::enabledGateways();
// ['wave', 'stripe', 'paydunya', 'paytech']
// Verifier si une passerelle est active
if (AfriPay::isEnabled('orange_money')) {
// ...
}# PRODUCTION (recommande) — seul le webhook peut confirmer un paiement
AFRIPAY_TRUST_WEBHOOK_ONLY=true
# DEVELOPPEMENT — l'URL de retour peut aussi confirmer
AFRIPAY_TRUST_WEBHOOK_ONLY=falseQuand trust_webhook_only=true, verifyAndProcess() verifie le statut aupres de la passerelle mais ne dispatche aucun evenement. Seul le webhook declenche PaymentCompleted. C'est plus sur car ca empeche un utilisateur de forger une URL de succes.
Quand trust_webhook_only=false, les deux chemins (webhook ET URL de retour) peuvent declencher les evenements. Utile en dev local quand les webhooks ne peuvent pas atteindre votre machine.
// Dans un ServiceProvider
use SunuCode\AfriPay\PaymentManager;
PaymentManager::extend('cinetpay', function (array $config) {
return new CinetPayGateway($config);
});
// Utilisation
AfriPay::via('cinetpay')->charge([...]);Les webhooks sont automatiquement enregistres a :
POST /afripay/webhooks/wave
POST /afripay/webhooks/stripe
POST /afripay/webhooks/paydunya
POST /afripay/webhooks/orange-money
POST /afripay/webhooks/paytech
POST /afripay/webhooks/paypal
Le chemin est configurable via AFRIPAY_WEBHOOK_PATH.
Chaque webhook :
- Verifie la signature (HMAC-SHA256 pour Wave/Stripe/PayTech, master_key pour PayDunya)
- Verifie le montant (tolerance +/- 1 unite)
- Utilise
lockForUpdate()pour eviter les doublons - Dispatche
PaymentCompletedouPaymentFailed
- Idempotence : Le champ
processed_atempeche le double-traitement - Verrouillage DB :
lockForUpdate()sur chaque transaction pendant le webhook - Verification de montant : Tolerance +/- 1 unite avant d'accepter
- Anti-replay : Timestamps verifies (Wave, Stripe) avec tolerance de 5 min
- Zero-decimal : XOF/XAF geres automatiquement (pas de x100 pour Stripe)
- Orange Money : Contre-verification API obligatoire (pas de signature webhook)
| Evenement | Quand | Donnees |
|---|---|---|
PaymentInitiated |
Apres charge() |
$transaction, $gateway |
PaymentCompleted |
Webhook confirme | $transaction |
PaymentFailed |
Webhook echoue | $transaction |
PaymentRefunded |
Apres refund() |
$transaction, $reason |
West African developers manually integrate each payment gateway in every project. Wave, Orange Money, PayDunya, PayTech... each with its own API, webhooks, and signatures.
AfriPay unifies everything into a single interface:
$payment = AfriPay::via('wave')->charge([
'amount' => 15000,
'currency' => 'XOF',
'description' => 'Premium Subscription',
'success_url' => route('payment.success'),
'error_url' => route('payment.error'),
]);
return redirect($payment['redirect_url']);composer require sunucode/afripay
php artisan vendor:publish --tag=afripay-config
php artisan migrateThis is the primary way to react to payment outcomes:
use SunuCode\AfriPay\Events\PaymentCompleted;
class ActivateSubscription
{
public function handle(PaymentCompleted $event): void
{
$transaction = $event->transaction;
$subscription = $transaction->payable;
$subscription->activate();
}
}Extend AfriPay with your own gateways:
use SunuCode\AfriPay\Contracts\GatewayInterface;
use SunuCode\AfriPay\PaymentManager;
class CinetPayGateway implements GatewayInterface
{
// Implement the 4 methods: charge(), handleWebhook(), verify(), verifySignature()
}
PaymentManager::extend('cinetpay', fn($config) => new CinetPayGateway($config));- Idempotent processing via atomic
processed_atflag - Database locking (
lockForUpdate) prevents race conditions - Amount verification with configurable tolerance
- Replay protection with timestamp validation (Wave, Stripe)
- Zero-decimal currencies (XOF, XAF) handled automatically
- Orange Money: Mandatory API counter-verification (no webhook signature)
- PHP >= 8.2
- Laravel 11, 12, or 13
- A database supporting
lockForUpdate()(MySQL, PostgreSQL)
Contributions are welcome! Please submit pull requests to the main branch.
- Built by Sunu Code — Software agency based in Dakar, Senegal
- Extracted from Semplio — Business management SaaS for African SMEs
MIT License. See LICENSE for details.