PHP code example of siberfx / mpesa-payment

1. Go to this page and download the library: Download siberfx/mpesa-payment library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

siberfx / mpesa-payment example snippets


use Siberfx\MpesaPayment\Mpesa;

$mpesa = Mpesa::make([
    'environment' => 'sandbox',            // or 'production'
    'consumer_key' => 'your-consumer-key',
    'consumer_secret' => 'your-consumer-secret',
    'shortcode' => '174379',
    'passkey' => 'your-passkey',
    'callbacks' => [
        'stk' => 'https://example.com/payments/stk',
    ],
]);

$response = $mpesa->stk()->push(
    amount: 100,
    phone: '0712 345 678',
    reference: 'INV-1001',
    description: 'Order #1001',
);

if ($response->accepted()) {
    $checkoutId = $response->checkoutRequestId(); // store it, the result arrives on your callback
}

$mpesa = Mpesa::make([
    'default' => 'kenya',
    'accounts' => [
        'kenya' => [
            'driver' => 'daraja',
            'environment' => 'production',
            'consumer_key' => '...',
            'consumer_secret' => '...',
            'shortcode' => '600000',
            'passkey' => '...',
            'initiator_name' => 'apiuser',
            'initiator_password' => '...',
            'certificate_path' => '/path/to/ProductionCertificate.cer',
            'callbacks' => [
                'stk' => 'https://example.com/payments/stk',
                'result' => 'https://example.com/payments/result',
                'timeout' => 'https://example.com/payments/timeout',
            ],
        ],
        'tanzania' => [
            'driver' => 'openapi',
            'environment' => 'sandbox',
            'market' => 'vodacomTZN',       // vodacomTZN | vodacomDRC | vodacomLES
            'api_key' => '...',
            'public_key' => '...',          // as issued by the OpenAPI portal
            'service_provider_code' => '000000',
        ],
    ],
    'http' => ['timeout' => 30, 'retries' => 2],
], cache: $psr16Cache, logger: $psr3Logger);

$mpesa->stk()->push(...);                     // default account
$mpesa->daraja('kenya')->b2c()->pay(...);     // explicit Daraja account
$mpesa->openApi('tanzania')->c2b(...);        // Vodacom market

use Siberfx\MpesaPayment\Laravel\Facades\Mpesa;

Mpesa::stk()->push(amount: 250, phone: $user->phone, reference: 'ORDER-42');
Mpesa::account('tanzania')->c2b(5000, '0754123456', 'ORDER-42');

use Siberfx\MpesaPayment\Laravel\Events\StkCallbackReceived;

Event::listen(function (StkCallbackReceived $event) {
    $callback = $event->callback;

    $order = Order::where('checkout_request_id', $callback->checkoutRequestId)->firstOrFail();

    $callback->successful()
        ? $order->markPaid($callback->receiptNumber(), $callback->amount())
        : $order->markFailed($callback->resultDescription);
});

use Siberfx\MpesaPayment\Daraja\Callbacks\C2BRejection;
use Siberfx\MpesaPayment\Daraja\Callbacks\C2BTransaction;
use Siberfx\MpesaPayment\Laravel\C2BValidation;

// AppServiceProvider::boot()
C2BValidation::using(function (C2BTransaction $payment, ?string $account) {
    return Invoice::where('number', $payment->billReference)->exists()
        ?: C2BRejection::InvalidAccountNumber;
});

use Siberfx\MpesaPayment\Enums\TransactionType;

$mpesa->stk()->push(100, '0712345678', 'INV-1');                                 // Pay Bill
$mpesa->stk()->push(100, '0712345678', 'INV-1', type: TransactionType::BuyGoods); // Buy Goods (till_number)
$mpesa->stk()->query($checkoutRequestId);

$mpesa->c2b()->registerUrls();                         // uses configured confirmation/validation URLs
$mpesa->c2b()->simulate(100, '254708374149', 'ACC-1'); // sandbox only

use Siberfx\MpesaPayment\Enums\CommandId;

$mpesa->b2c()->pay(
    amount: 1500,
    phone: '0712345678',
    remarks: 'March salary',
    command: CommandId::SalaryPayment,
    originatorConversationId: $payout->uuid, // optional, recommended for traceability
);

$mpesa->b2b()->pay(5000, receiver: '600000', accountReference: 'INV-7', command: CommandId::BusinessPayBill);
$mpesa->b2b()->pay(5000, receiver: '123456', accountReference: 'INV-7', command: CommandId::BusinessBuyGoods);

// Ask the merchant owning till 000001 to pay you via a USSD prompt
$mpesa->b2b()->expressCheckout(primaryShortcode: '000001', amount: 5000, paymentReference: 'INV-7', partnerName: 'Acme Ltd');

$mpesa->balance()->balance();
$mpesa->balance()->transactionStatus(transactionId: 'OEI2AK4Q16');
$mpesa->balance()->transactionStatus(originatorConversationId: $uuid);
$mpesa->balance()->reverse('OEI2AK4Q16', amount: 100);

use Siberfx\MpesaPayment\Daraja\Services\DynamicQr;
use Siberfx\MpesaPayment\Enums\QrTransactionCode;

$response = $mpesa->qr()->generate(250, merchantName: 'My Shop', reference: 'INV-9', code: QrTransactionCode::BuyGoods);

echo '<img src="'.DynamicQr::dataUri($response).'">';

$mpesa->tax()->remit(amount: 12000, paymentRegistrationNumber: 'PRN1234XN');

use Siberfx\MpesaPayment\Enums\StandingOrderFrequency;

$mpesa->ratiba()->create(
    name: 'Gym membership',
    amount: 2500,
    phone: '0712345678',
    startDate: new DateTimeImmutable('2026-11-01'),
    endDate: new DateTimeImmutable('2027-10-31'),
    frequency: StandingOrderFrequency::Monthly,
    accountReference: 'MEMBER-88',
);

$tz = $mpesa->openApi('tanzania');

$tz->c2b(10000, '0754123456', reference: 'ORDER-1', description: 'Shoes');
$tz->b2c(5000, '0754123456', reference: 'PAYOUT-1');
$tz->b2b(5000, receiverPartyCode: '000001', reference: 'INV-1');
$tz->reverse($transactionId, 10000);
$tz->status($transactionId);

use Siberfx\MpesaPayment\Daraja\Callbacks\{StkCallback, C2BTransaction, ResultCallback};

$payload = json_decode(file_get_contents('php://input'), true);

$callback = StkCallback::fromArray($payload);
$callback->successful();
$callback->receiptNumber();
$callback->amount();
$callback->phoneNumber();
$callback->transactionDate();

// C2B validation endpoint
echo json_encode(C2BTransaction::accept());

Schema::create('payments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('order_id')->constrained();
    $table->string('phone');
    $table->unsignedInteger('amount');
    $table->string('status')->default('pending'); // pending | paid | failed
    $table->string('checkout_request_id')->nullable()->unique();
    $table->string('receipt_number')->nullable()->unique();
    $table->string('failure_reason')->nullable();
    $table->timestamps();
});

use Siberfx\MpesaPayment\Exceptions\MpesaException;
use Siberfx\MpesaPayment\Exceptions\ValidationException;
use Siberfx\MpesaPayment\Laravel\Facades\Mpesa;
use Siberfx\MpesaPayment\Support\Phone;

final class CheckoutController
{
    public function pay(Request $request, Order $order)
    {
        $data = $request->validate(['phone' => ['       ]);

        try {
            $response = Mpesa::stk()->push(
                amount: $payment->amount,
                phone: $payment->phone,
                reference: 'ORD'.$order->id,          // max 12 characters
                description: 'Order '.$order->id,
            );
        } catch (ValidationException $e) {
            $payment->update(['status' => 'failed', 'failure_reason' => $e->getMessage()]);

            return back()->withErrors(['phone' => $e->getMessage()]);
        } catch (MpesaException $e) {
            report($e);
            $payment->update(['status' => 'failed', 'failure_reason' => 'Gateway unavailable']);

            return back()->withErrors(['phone' => 'M-Pesa is unavailable, please try again.']);
        }

        $payment->update(['checkout_request_id' => $response->checkoutRequestId()]);

        // The customer now sees a PIN prompt. Nothing is paid yet.
        CheckStkPayment::dispatch($payment)->delay(now()->addSeconds(90));

        return redirect()->route('orders.waiting', $order);
    }
}

use Illuminate\Contracts\Queue\ShouldQueue;
use Siberfx\MpesaPayment\Laravel\Events\StkCallbackReceived;

final class RecordStkPayment implements ShouldQueue
{
    public function handle(StkCallbackReceived $event): void
    {
        $callback = $event->callback;

        DB::transaction(function () use ($callback) {
            $payment = Payment::where('checkout_request_id', $callback->checkoutRequestId)
                ->lockForUpdate()
                ->first();

            // Unknown or already processed (callbacks can be delivered more than once).
            if ($payment === null || $payment->status !== 'pending') {
                return;
            }

            if (! $callback->successful()) {
                $payment->update(['status' => 'failed', 'failure_reason' => $callback->resultDescription]);

                return;
            }

            // Never trust that the amount matches: verify it.
            if ((int) $callback->amount() !== $payment->amount) {
                $payment->update(['status' => 'failed', 'failure_reason' => 'Amount mismatch']);
                report(new RuntimeException("STK amount mismatch for payment {$payment->id}"));

                return;
            }

            $payment->update(['status' => 'paid', 'receipt_number' => $callback->receiptNumber()]);
            $payment->order->markAsPaid();
        });
    }
}

use Siberfx\MpesaPayment\Exceptions\RequestException;
use Siberfx\MpesaPayment\Laravel\Facades\Mpesa;

final class CheckStkPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;

    public function __construct(public Payment $payment) {}

    public function handle(): void
    {
        if ($this->payment->fresh()->status !== 'pending') {
            return; // the callback already settled it
        }

        try {
            $result = Mpesa::stk()->query($this->payment->checkout_request_id);
        } catch (RequestException $e) {
            // "The transaction is being processed" — ask again later.
            $this->release(60);

            return;
        }

        if ((string) $result->get('ResultCode') === '0') {
            // Paid, but the query does not return the receipt number:
            // mark it paid and reconcile the receipt from your M-Pesa statement if needed.
            $this->payment->update(['status' => 'paid']);
        } else {
            $this->payment->update(['status' => 'failed', 'failure_reason' => $result->get('ResultDesc')]);
        }
    }
}

// AppServiceProvider::boot()
C2BValidation::using(function (C2BTransaction $payment) {
    $invoice = Invoice::where('number', strtoupper(trim($payment->billReference)))->first();

    return match (true) {
        $invoice === null => C2BRejection::InvalidAccountNumber,
        $payment->amount < $invoice->balance => C2BRejection::InvalidAmount,
        default => true,
    };
});

// Listener
Event::listen(function (C2BConfirmationReceived $event) {
    $tx = $event->transaction;

    // TransID is unique: use it to make the handler idempotent.
    InvoicePayment::firstOrCreate(
        ['mpesa_transaction_id' => $tx->transactionId],
        [
            'invoice_number' => strtoupper(trim($tx->billReference)),
            'amount' => $tx->amount,
            'payer_name' => $tx->customerName(),
            'paid_at' => $tx->transactedAt(),
        ],
    );
});

use Siberfx\MpesaPayment\Enums\CommandId;
use Siberfx\MpesaPayment\Exceptions\ConnectionException;
use Siberfx\MpesaPayment\Laravel\Events\ResultReceived;

$payout = Payout::create(['uuid' => (string) Str::uuid(), 'phone' => $phone, 'amount' => 2500]);

try {
    $response = Mpesa::b2c()->pay(
        amount: $payout->amount,
        phone: $payout->phone,
        remarks: 'Commission',
        command: CommandId::BusinessPayment,
        originatorConversationId: $payout->uuid,
        resultUrl: route('mpesa.result', ['type' => 'b2c']).'?token='.config('mpesa.security.token'),
    );

    $payout->update(['conversation_id' => $response->conversationId(), 'status' => 'submitted']);
} catch (ConnectionException $e) {
    // Unknown outcome: do NOT send again. Check the status first (see pitfalls below).
    $payout->update(['status' => 'unknown']);
}

Event::listen(function (ResultReceived $event) {
    if ($event->type !== 'b2c') {
        return;
    }

    $payout = Payout::where('uuid', $event->result->originatorConversationId)->first();

    $payout?->update($event->result->successful()
        ? ['status' => 'paid', 'receipt' => $event->result->transactionId]
        : ['status' => 'failed', 'failure_reason' => $event->result->resultDescription]);
});

// bootstrap.php
use Siberfx\MpesaPayment\Mpesa;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;

$mpesa = Mpesa::make(
    ;

// callback.php — the URL you configured as the "stk" callback
use Siberfx\MpesaPayment\Daraja\Callbacks\StkCallback;
use Siberfx\MpesaPayment\Exceptions\ValidationException;

if (! hash_equals(getenv('MPESA_CALLBACK_TOKEN'), $_GET['token'] ?? '')) {
    http_response_code(403);
    exit;
}

try {
    $callback = StkCallback::fromArray(json_decode(file_get_contents('php://input'), true) ?? []);
} catch (ValidationException) {
    http_response_code(422);
    exit;
}

$pdo->prepare('UPDATE payments SET status = ?, receipt = ? WHERE checkout_request_id = ? AND status = "pending"')
    ->execute([$callback->successful() ? 'paid' : 'failed', $callback->receiptNumber(), $callback->checkoutRequestId]);

header('Content-Type: application/json');
echo json_encode(['ResultCode' => 0, 'ResultDesc' => 'Accepted']);

use Siberfx\MpesaPayment\Exceptions\RequestException;

$tz = Mpesa::openApi('tanzania');

try {
    $response = $tz->c2b(
        amount: 15000,
        phone: '0754 123 456',
        reference: 'ORDER-77',
        description: 'Order 77',
        conversationId: $order->uuid,
    );

    $order->markAsPaid($response->transactionId());
} catch (RequestException $e) {
    // e.g. INS-2006 insufficient balance, INS-9 request timeout (customer did not enter the PIN)
    $order->markAsFailed($e->errorCode, $e->getMessage());
}

// routes/console.php
Schedule::call(fn () => Mpesa::balance()->balance(remarks: 'Daily balance'))->dailyAt('07:00');

Event::listen(function (ResultReceived $event) {
    foreach ($event->result->balances() as $row) {
        if ($row['account'] === 'Working Account' && $row['available'] < 50_000) {
            Notification::route('mail', '[email protected]')->notify(new LowFloatAlert($row));
        }
    }
});
bash
php artisan vendor:publish --tag=mpesa-config
bash
php artisan mpesa:register-urls --account=kenya
bash
php artisan mpesa:register-urls --response-type=Cancelled