vnuswilliams/laravel-kpay

Laravel integration for KPay - Mobile Money payment aggregator for Africa

Maintainers

Package info

github.com/vnuswilliams/laravel-kpay

pkg:composer/vnuswilliams/laravel-kpay

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.1 2026-07-23 03:25 UTC

This package is auto-updated.

Last update: 2026-07-23 03:49:25 UTC


README

Intégration Laravel pour KPay — Agrégateur de paiements Mobile Money pour l'Afrique.

English Version

Latest Stable Version License

Fonctionnalités

  • Paiements (USSD & Gateway) — Encaissez via MTN MoMo, Orange Money, M-Pesa et 20+ opérateurs dans 13 pays africains
  • Retraits (USSD & Gateway) — Envoyez de l'argent sur des comptes Mobile Money, y compris transfrontaliers avec conversion automatique
  • Transferts entre portefeuilles — Déplacez des fonds entre les portefeuilles pays avec des taux de change en temps réel
  • Webhooks — Traitez les mises à jour de statut de manière asynchrone avec support queue et vérification de signature
  • Modèles Eloquent — Stockez et interrogez paiements et retraits localement
  • Événements — Réagissez à PaymentCompleted, PayoutFailed et autres changements de statut
  • API Fluide — Syntaxe chainable et expressive pour toutes les opérations
  • Commandes Artisan — Vérifiez les soldes, la disponibilité des opérateurs et testez les webhooks

Prérequis

  • PHP 8.2+
  • Laravel 12.x ou 13.x

Installation

composer require vnuswilliams/laravel-kpay
php artisan vendor:publish --tag="kpay"
php artisan migrate

Configuration .env

Ajoutez ces variables à votre fichier .env. Chaque variable a un rôle précis :

# ─── Clés API (obtenues sur admin.kpay.site) ────────────────────────────
# La clé API identifie votre application. Le préfixe détermine l'environnement :
#   kpay_test_xxx  → Sandbox (aucune vraie argent ne bouge)
#   kpay_live_xxx   → Production (transactions réelles)
KPAY_API_KEY=kpay_test_xxxxxxxxxxxxxxxx

# La clé secrète sert à authentifier vos requêtes vers l'API KPay.
# Ne JAMAIS la commiter dans Git.
KPAY_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxx

# ─── Secrets de vérification ──────────────────────────────────────────────
# Secret pour vérifier la signature HMAC-SHA256 des webhooks reçus de KPay.
# À configurer dans le dashboard KPay (Section Webhooks).
KPAY_WEBHOOK_SECRET=your_webhook_secret_here

# Secret pour vérifier les signatures des URLs de retour Gateway.
# À configurer dans le dashboard KPay (Section Gateway).
KPAY_GATEWAY_SECRET=your_gateway_secret_here

# ─── Configuration technique ──────────────────────────────────────────────
# URL de base de l'API KPay (ne pas modifier sauf instance personnalisée)
KPAY_BASE_URL=https://admin.kpay.site

# Timeout des requêtes API en secondes
KPAY_TIMEOUT=30

# ─── Queue (pour le traitement asynchrone des webhooks) ───────────────────
# Active le traitement webhook via la queue Laravel (recommandé en production)
KPAY_QUEUE_ENABLED=true

# Connection queue à utiliser (redis, database, sqs, etc.)
KPAY_QUEUE_CONNECTION=database

# Nom de la queue dédiée aux webhooks KPay
KPAY_QUEUE_NAME=kpay

# ─── URLs Gateway par défaut ──────────────────────────────────────────────
# URLs utilisées automatiquement pour les paiements/retraits Gateway
# quand ->returnUrl() ou ->cancelUrl() n'est pas appelé explicitement.
# Vous pouvez quand même les surcharger par appel avec ->returnUrl() / ->cancelUrl().
KPAY_RETURN_URL=https://mysite.com/payment/return
KPAY_CANCEL_URL=https://mysite.com/payment/cancel

Important : Les clés KPAY_API_KEY et KPAY_SECRET_KEY s'obtiennent sur admin.kpay.site après avoir créé un compte et une application.

Démarrage rapide

1. Encaisser un paiement USSD

Le mode USSD envoie une notification push directement sur le téléphone du client. Le client reçoit un popup USSD et confirme avec son PIN.

use VnusWilliams\KPay\Facades\KPay;

$payment = KPay::payment()
    ->amount(5000)                              // Montant brut (minimum 500 XAF au Cameroun)
    ->provider('MTN_MOMO_CMR')                  // Opérateur Mobile Money
    ->phoneNumber('237670000001')                // Numéro du client (format international)
    ->externalId('ORDER-12345')                  // Votre ID unique (idempotence)
    ->description('Achat t-shirt KPay')         // Description lisible
    ->customerName('Jean Dupont')                // Nom du client
    ->customerEmail('jean@example.com')          // Email du client
    ->metadata([                                 // Métadonnées libres (optionnel)
        'product_id' => 42,
        'size' => 'L',
    ])
    ->create();                                  // → PaymentData

// Résultat :
// $payment->id             → "pay_abc123def456" (ID KPay)
// $payment->reference      → "KPAY-XYZ-789"     (référence KPay)
// $payment->status         → PaymentStatus::PENDING
// $payment->amount         → 5000.0
// $payment->currency       → "XAF"
// $payment->netAmount      → 4750.0  (après commission)
// $payment->feeAmount      → 250.0   (commission KPay)
// $payment->isTest         → true    (si clés sandbox)

// Le client reçoit un popup USSD sur son téléphone.
// Une fois le paiement confirmé/échoué/annulé, KPay envoie un webhook.

Comment ça marche :

  1. Vous appelez create() → le package envoie une requête POST à l'API KPay
  2. KPay retourne un PaymentData avec le statut PENDING
  3. Le client reçoit un popup USSD sur son téléphone avec le montant et la description
  4. Le client confirme en entrant son PIN Mobile Money
  5. KPay met à jour le statut et envoie un webhook à votre endpoint /kpay/webhook

2. Encaisser via Gateway (page hébergée)

Le mode Gateway héberge une page de paiement sur KPay. Le client choisit lui-même son opérateur et son numéro sur la page hébergée — vous ne passez ni provider ni phoneNumber.

Les URLs de retour (KPAY_RETURN_URL / KPAY_CANCEL_URL) sont lues automatiquement depuis votre .env.

use VnusWilliams\KPay\Facades\KPay;

// Minimal — le client choisit opérateur et numéro sur la page KPay
$payment = KPay::payment()
    ->amount(5000)
    ->externalId('ORDER-12346')
    ->createGateway();

// Résultat :
// $payment->gatewayUrl  → "https://gateway.kpay.site/pay/abc123..."
// $payment->mode        → PaymentMode::GATEWAY

// Redirigez le client vers la page de paiement :
return redirect($payment->gatewayUrl);

Attention : En mode Gateway, provider, phoneNumber et customerName sont interdits. Le package lève une ValidationException si vous les passez. Le client les saisit lui-même sur la page hébergée KPay.

Si vous avez besoin de surcharger les URLs pour une commande spécifique (ex: ajouter un paramètre order_id) :

$payment = KPay::payment()
    ->amount(5000)
    ->externalId('ORDER-12346')
    ->returnUrl(route('payment.return', ['order' => 12346]))  // surcharge pour cette commande
    ->cancelUrl(route('payment.cancel', ['order' => 12346]))
    ->createGateway();

Comment ça marche :

  1. Vous appelez createGateway() → KPay retourne une URL de paiement
  2. Les URLs de retour sont lues depuis le .env (ou surchargées via les builders)
  3. Vous redirigez le client vers $payment->gatewayUrl
  4. Le client choisit lui-même son opérateur et entre son numéro sur la page KPay
  5. Le client confirme avec son PIN Mobile Money
  6. Après paiement, KPay redirige le client vers votre returnUrl
  7. Le webhook /kpay/webhook est appelé pour confirmer le statut

3. Envoyer un retrait (payout)

$payout = KPay::payout()
    ->amount(10000)                             // Montant à retirer
    ->provider('MTN_MOMO_CMR')                  // Opérateur du bénéficiaire
    ->phoneNumber('237670000002')                // Numéro du bénéficiaire
    ->externalId('WD-98765')                     // Votre ID unique
    ->description('Retrait commission vendeur')
    ->create();                                  // → PayoutData

// Vérifiez le solde AVANT de faire un retrait :
$balances = KPay::balance();
$xafWallet = $balances->first(fn ($b) => $b->currency === 'XAF');
if ($xafWallet->availableBalance < 10000) {
    throw new \Exception('Solde insuffisant pour ce retrait');
}

4. Retrait transfrontalier

$payout = KPay::payout()
    ->amount(50000)                              // Montant en XAF (devise source)
    ->provider('ORANGE_SEN')                     // Opérateur au Sénégal
    ->phoneNumber('221770000003')                // Numéro sénégalais
    ->sourceCountry('CMR')                       // Retirer du portefeuille Cameroun
    ->externalId('PAYOUT-CROSS-001')
    ->create();

// Résultat :
// $payout->payoutCurrency  → "XOF"   (devise du Sénégal)
// $payout->exchangeRate    → 0.654   (taux XAF → XOF)
// $payout->payoutAmount    → 32700   (montant crédité au Sénégal)

5. Transfert entre portefeuilles

// D'abord, récupérez votre applicationId :
$appInfo = KPay::applicationInfo();
$applicationId = $appInfo->id;

$transfer = KPay::wallet()
    ->applicationId($applicationId)             // UUID de votre application
    ->fromCountry('CMR')                        // Portefeuille source (Cameroun)
    ->toCountry('SEN')                          // Portefeuille destination (Sénégal)
    ->amount(100000)                            // Montant en XAF
    ->externalId('TRF-2026-001')
    ->description('Approvisionnement portefeuille Sénégal')
    ->transfer();                               // → WalletTransferData

Configuration des Webhooks

Étape 1 : Endpoint

L'endpoint POST /kpay/webhook est enregistré automatiquement par le package. Aucune configuration de route n'est nécessaire.

Étape 2 : Configurer le secret dans .env

KPAY_WEBHOOK_SECRET=votre_secret_webhook_ici

Ce secret doit correspondre à celui configuré dans votre dashboard KPay. Il sert à vérifier la signature HMAC-SHA256 de chaque webhook reçu.

Étape 3 : Écouter les événements

Dans AppServiceProvider::boot() ou EventServiceProvider :

use VnusWilliams\KPay\Events\PaymentCompleted;
use VnusWilliams\KPay\Events\PaymentFailed;
use VnusWilliams\KPay\Events\PaymentCancelled;
use VnusWilliams\KPay\Events\PayoutCompleted;
use VnusWilliams\KPay\Events\PayoutFailed;

// ✅ Paiement réussi → Marquer la commande comme payée
Event::listen(PaymentCompleted::class, function (PaymentCompleted $event) {
    $webhook = $event->event;  // WebhookEvent DTO

    // Mettre à jour votre commande
    Order::where('external_id', $webhook->externalId)
        ->update(['status' => 'paid']);

    // Envoyer un email de confirmation
    Mail::to($customer->email)->send(new PaymentConfirmation($webhook));
});

// ❌ Paiement échoué → Notifier le client
Event::listen(PaymentFailed::class, function (PaymentFailed $event) {
    $webhook = $event->event;

    Log::warning("Paiement échoué: {$webhook->failureReason}", [
        'external_id' => $webhook->externalId,
        'reference' => $webhook->reference,
    ]);
});

// ❌ Paiement annulé
Event::listen(PayoutFailed::class, function (PayoutFailed $event) {
    // Gérer l'échec du retrait
    $webhook = $event->event;
    Log::error("Retrait échoué: {$webhook->failureReason}");
});

// ✅ Retrait réussi
Event::listen(PayoutCompleted::class, function (PayoutCompleted $event) {
    $webhook = $event->event;

    // Mettre à jour votre ledger interne
    Ledger::where('external_id', $webhook->externalId)
        ->update(['status' => 'completed']);
});

Étape 4 : Queue (recommandé)

Le traitement webhook est dispatché en queue par défaut pour répondre rapidement à KPay (200 OK). Assurez-vous que votre worker queue est actif :

php artisan queue:work --queue=kpay

Étape 5 : Tester le webhook

php artisan kpay:test-webhook https://mysite.com/kpay/webhook --event=payment.completed --status=COMPLETED

Utilitaires

use VnusWilliams\KPay\Facades\KPay;

// 💰 Vérifier les soldes de tous les portefeuilles
$balances = KPay::balance();
foreach ($balances as $wallet) {
    echo "{$wallet->currency}: {$wallet->availableBalance} disponible";
}

// 🌍 Vérifier la disponibilité des opérateurs par pays
$availability = KPay::availability();
foreach ($availability->countries as $country) {
    echo "Pays: {$country->country}\n";
    foreach ($country->providers as $provider) {
        echo "  {$provider->provider}: {$provider->operationTypes[0]['status']}\n";
    }
}

// 💱 Obtenir le taux de change entre deux devises
$rate = KPay::exchangeRate('XAF', 'XOF');
$montantConverti = $rate->convert(10000);  // → 10153.0

// 📱 Prédire l'opérateur à partir d'un numéro de téléphone
$prediction = KPay::predictProvider('237670000001');
// $prediction->provider → "MTN_MOMO_CMR"
// $prediction->country  → "CMR"

// 🏢 Infos de l'application (pour obtenir l'applicationId)
$appInfo = KPay::applicationInfo();
echo $appInfo->id;  // UUID de votre application

Gestion des erreurs

use VnusWilliams\KPay\Exceptions\ConflictException;
use VnusWilliams\KPay\Exceptions\InsufficientBalanceException;
use VnusWilliams\KPay\Exceptions\ValidationException;
use VnusWilliams\KPay\Exceptions\KPayException;
use VnusWilliams\KPay\Exceptions\AuthenticationException;

try {
    $payment = KPay::payment()
        ->amount(5000)
        ->provider('MTN_MOMO_CMR')
        ->phoneNumber('237670000001')
        ->externalId('ORDER-12345')
        ->create();

} catch (ConflictException $e) {
    // L'externalId est déjà utilisé (idempotence)
    // → Récupérer le paiement existant avec KPay::paymentStatus()

} catch (InsufficientBalanceException $e) {
    // Solde insuffisant dans le portefeuille KPay
    // → Notifier l'admin ou réessayer plus tard

} catch (ValidationException $e) {
    // Paramètres invalides (montant trop bas, numéro invalide, etc.)
    // → Corriger les paramètres et réessayer

} catch (AuthenticationException $e) {
    // Clés API invalides ou expirées
    // → Vérifier KPAY_API_KEY et KPAY_SECRET_KEY dans .env

} catch (KPayException $e) {
    // Erreur API générale
    Log::error('Erreur KPay', [
        'status' => $e->getStatusCode(),
        'message' => $e->getMessage(),
    ]);
}

Modèles Eloquent

Les paiements et retraits sont automatiquement stockés en base de données via les migrations publiées.

use VnusWilliams\KPay\Models\KPayPayment;
use VnusWilliams\KPay\Models\KPayPayout;

// Rechercher par statut
$completed = KPayPayment::completed()->get();
$pending = KPayPayout::pending()->get();

// Rechercher par external_id
$payment = KPayPayment::where('external_id', 'ORDER-12345')->first();

// Vérifier si un paiement est terminé
if ($payment->isTerminal()) {
    // Le paiement est COMPLETED, FAILED ou CANCELLED
}

// Vérifier si c'est un paiement réussi
if ($payment->isSuccess()) {
    // Le paiement est COMPLETED
}

// Vérifier si c'est un paiement Gateway
if ($payment->isGateway()) {
    echo $payment->gateway_url;
}

Commandes Artisan

# Afficher les soldes de tous les portefeuilles
php artisan kpay:check-balance

# Vérifier la disponibilité des opérateurs par pays
php artisan kpay:check-availability

# Envoyer un webhook test signé à votre endpoint
php artisan kpay:test-webhook https://mysite.com/kpay/webhook
php artisan kpay:test-webhook https://mysite.com/kpay/webhook --event=payout.completed --status=COMPLETED

Exemple complet : Intégrer KPay dans un contrôleur Laravel

Voici un exemple concret d'intégration dans un contrôleur de e-commerce :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;
use VnusWilliams\KPay\Facades\KPay;
use VnusWilliams\KPay\Exceptions\KPayException;

class PaymentController extends Controller
{
    /**
     * Initier le paiement d'une commande.
     */
    public function store(Request $request)
    {
        $request->validate([
            'order_id' => 'required|exists:orders,id',
            'phone_number' => 'required|string',
            'provider' => 'required|string',
        ]);

        $order = $request->user()->orders()->findOrFail($request->order_id);

        // Générer un ID unique pour cette transaction
        $externalId = 'ORDER-' . $order->id . '-' . Str::random(8);

        try {
            $payment = KPay::payment()
                ->amount($order->total_amount)
                ->provider($request->provider)
                ->phoneNumber($request->phone_number)
                ->externalId($externalId)
                ->description("Commande #{$order->id}")
                ->customerName($request->user()->name)
                ->customerEmail($request->user()->email)
                ->metadata([
                    'order_id' => $order->id,
                    'items_count' => $order->items->count(),
                ])
                ->create();

            // Stocker la référence KPay dans la commande
            $order->update([
                'kpay_reference' => $payment->reference,
                'kpay_external_id' => $externalId,
                'payment_status' => 'pending',
            ]);

            return response()->json([
                'message' => 'Paiement initié. Vérifiez votre téléphone.',
                'payment_id' => $payment->id,
                'status' => $payment->status->value,
            ]);

        } catch (KPayException $e) {
            return response()->json([
                'message' => 'Erreur lors de l\'initiation du paiement.',
                'error' => $e->getMessage(),
            ], 422);
        }
    }

    /**
     * Vérifier le statut d'un paiement.
     */
    public function show(string $externalId)
    {
        $order = auth()->user()->orders()
            ->where('kpay_external_id', $externalId)
            ->firstOrFail();

        // Interroger l'API KPay pour le statut en temps réel
        $payment = KPay::paymentStatus($order->kpay_reference);

        return response()->json([
            'status' => $payment->status->value,
            'amount' => $payment->amount,
            'currency' => $payment->currency,
            'completed_at' => $payment->completedAt,
        ]);
    }
}

Dans EventServiceProvider ou AppServiceProvider::boot() :

use VnusWilliams\KPay\Events\PaymentCompleted;

Event::listen(PaymentCompleted::class, function (PaymentCompleted $event) {
    $externalId = $event->event->externalId;

    // Extraire l'order_id depuis l'externalId (format: "ORDER-{id}-{hash}")
    $parts = explode('-', $externalId);
    $orderId = $parts[1] ?? null;

    if ($orderId) {
        Order::where('id', $orderId)->update([
            'payment_status' => 'paid',
            'paid_at' => now(),
        ]);

        // Déclencher la logique post-paiement (envoi email, préparation commande...)
    }
});

Pays et opérateurs supportés

Pays Opérateur Code Devise Décimales
Bénin MTN MTN_MOMO_BEN XOF Non
Bénin Moov MOOV_BEN XOF Non
Cameroun MTN MTN_MOMO_CMR XAF Non
Cameroun Orange ORANGE_CMR XAF Non
Côte d'Ivoire MTN MTN_MOMO_CIV XOF Non
Côte d'Ivoire Orange ORANGE_CIV XOF Non
RD Congo Vodacom VODACOM_MPESA_COD CDF Non
RD Congo Airtel AIRTEL_COD CDF Oui
RD Congo Orange ORANGE_COD CDF Oui
Gabon Airtel AIRTEL_GAB XAF Oui
Kenya M-Pesa MPESA_KEN KES Non
Congo Airtel AIRTEL_COG XAF Non
Congo MTN MTN_MOMO_COG XAF Non
Rwanda Airtel AIRTEL_RWA XAF Non
Rwanda MTN MTN_MOMO_RWA XAF Non
Sénégal Free FREE_SEN XOF Non
Sénégal Orange ORANGE_SEN XOF Non
Sierra Leone Orange ORANGE_SLE SLE Oui
Ouganda Airtel AIRTEL_OAPI_UGA UGX Non
Ouganda MTN MTN_MOMO_UGA UGX Oui
Zambie Airtel AIRTEL_OAPI_ZMB ZMW Oui
Zambie MTN MTN_MOMO_ZMB ZMW Oui
Zambie Zamtel ZAMTEL_ZMB ZMW Oui

Contribuer

Voir CONTRIBUTING.md pour les directives.

Licence

The MIT License (MIT). Voir LICENSE.md pour plus d'informations.