Skip to main content

PHP SDK

composer require palpluss/sdk
Requires PHP 8.1+ and the ext-json extension (standard in all modern PHP distributions).

Client initialisation

use PalPluss\PalPluss;

$client = new PalPluss(
    apiKey: 'pk_live_...',          // or reads PALPLUSS_API_KEY from $_ENV
    timeout: 30.0,
    autoRetryOnRateLimit: true,
    maxRetries: 3,
);

Framework examples

Register as a singleton in a service provider:
// app/Providers/PalPlussServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use PalPluss\PalPluss;

class PalPlussServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(PalPluss::class, function () {
            return new PalPluss(
                apiKey: config('services.palpluss.key'),
            );
        });
    }
}
Add the key in config/services.php:
'palpluss' => [
    'key' => env('PALPLUSS_API_KEY'),
],
Controller
// app/Http/Controllers/PaymentController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use PalPluss\PalPluss;
use PalPluss\Http\Errors\PalPlussApiError;

class PaymentController extends Controller
{
    public function __construct(private readonly PalPluss $palpluss) {}

    public function initiatePayment(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'amount'    => 'required|numeric|min:1',
            'phone'     => 'required|string',
            'reference' => 'nullable|string',
        ]);

        try {
            $tx = $this->palpluss->stkPush(
                amount: $validated['amount'],
                phone:  $validated['phone'],
                accountReference: $validated['reference'] ?? '',
                callbackUrl: route('webhooks.palpluss'),
            );

            return response()->json([
                'transaction_id' => $tx['transactionId'],
                'status'         => $tx['status'],
            ]);
        } catch (PalPlussApiError $e) {
            return response()->json([
                'error' => $e->getMessage(),
                'code'  => $e->errorCode,
            ], $e->httpStatus);
        }
    }
}
Webhook controller
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use PalPluss\Webhooks;

class WebhookController extends Controller
{
    public function handlePalPluss(Request $request): Response
    {
        try {
            $payload = Webhooks::parsePayload($request->getContent());
        } catch (\InvalidArgumentException) {
            return response('Invalid payload', 400);
        }

        $eventType   = $payload['event_type'];
        $transaction = $payload['transaction'];

        match ($eventType) {
            'transaction.success' => $this->onSuccess($transaction),
            'transaction.failed'  => $this->onFailed($transaction),
            default               => null,
        };

        return response('', 200);
    }

    private function onSuccess(array $transaction): void
    {
        // fulfil order, dispatch a job, etc.
        logger()->info('Payment confirmed', [
            'id'      => $transaction['id'],
            'receipt' => $transaction['mpesa_receipt'],
        ]);
    }

    private function onFailed(array $transaction): void
    {
        logger()->warning('Payment failed', [
            'id'   => $transaction['id'],
            'desc' => $transaction['result_desc'],
        ]);
    }
}
Routes
// routes/api.php
use App\Http\Controllers\PaymentController;
use App\Http\Controllers\WebhookController;

Route::post('/pay', [PaymentController::class, 'initiatePayment']);
Route::post('/webhooks/palpluss', [WebhookController::class, 'handlePalPluss'])
    ->name('webhooks.palpluss')
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
Queue jobs for async processingDispatching a job from the webhook endpoint keeps response times fast and prevents timeout retries:
// app/Jobs/ProcessPalPlussWebhook.php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class ProcessPalPlussWebhook implements ShouldQueue
{
    use Dispatchable, Queueable;

    public function __construct(public readonly array $payload) {}

    public function handle(): void
    {
        $eventType   = $this->payload['event_type'];
        $transaction = $this->payload['transaction'];

        if ($eventType === 'transaction.success') {
            // fulfil order
        }
    }
}
// In WebhookController::handlePalPluss — dispatch instead of processing inline
ProcessPalPlussWebhook::dispatch($payload);
return response('', 200);

STK Push

$tx = $client->stkPush(
    amount: 500,
    phone: '254712345678',
    accountReference: 'ORDER-001',
    transactionDesc: 'Payment for order #001',
    callbackUrl: 'https://yourapp.com/webhooks/palpluss',
);

echo $tx['transactionId'];  // save for polling / reconciliation
echo $tx['status'];         // "PENDING"

B2C Payout

Always supply an idempotencyKey so you can safely retry without double-paying:
$payout = $client->b2cPayout(
    amount: 1000,
    phone: '254712345678',
    reference: 'SALARY-JAN-2025-EMP001',
    description: 'January salary',
    idempotencyKey: 'salary-jan-2025-emp001',
);

echo $payout['transactionId'];
echo $payout['status'];  // "PENDING"

Service wallet

// Check balance
$balance = $client->getServiceBalance();
echo $balance['availableBalance'];  // e.g. 45000

// Top up via STK Push
$topup = $client->serviceTopup(
    amount: 10000,
    phone: '254712345678',
    idempotencyKey: 'topup-jan-2025-001',
);

Transactions

// Fetch one
$tx = $client->getTransaction('tx_01j8abc123');
echo $tx['status'];  // "SUCCESS"

// List with pagination
$cursor = null;

do {
    $page = $client->listTransactions(
        limit: 100,
        status: 'SUCCESS',
        type: 'STK',
        cursor: $cursor,
    );

    foreach ($page['items'] as $item) {
        echo $item['transaction_id'] . ' ' . $item['status'] . "\n";
    }

    $cursor = $page['next_cursor'] ?? null;
} while ($cursor !== null);

Payment wallet channels

// Create
$channel = $client->createChannel(
    type: 'PAYBILL',
    shortcode: '400200',
    name: 'Main Paybill',
    isDefault: true,
);

// Update
$client->updateChannel(
    channelId: $channel['id'],
    name: 'Primary Paybill',
);

// Delete
$client->deleteChannel($channel['id']);

Error handling

use PalPluss\Http\Errors\PalPlussApiError;
use PalPluss\Http\Errors\RateLimitError;

try {
    $result = $client->stkPush(amount: 500, phone: '254712345678');
} catch (RateLimitError $e) {
    // $e->retryAfter — seconds to wait before retrying
    echo "Rate limited — retry after {$e->retryAfter}s\n";
} catch (PalPlussApiError $e) {
    // $e->errorCode  — machine-readable code e.g. "INVALID_PHONE"
    // $e->httpStatus — HTTP status code       e.g. 400
    // $e->requestId  — include in support requests
    echo "[{$e->errorCode}] HTTP {$e->httpStatus}: {$e->getMessage()}\n";
    echo "Request ID: {$e->requestId}\n";
}

Common error codes

CodeHTTPMeaning
INVALID_PHONE400Phone number format not recognised
INSUFFICIENT_SERVICE_BALANCE402Not enough service tokens to cover the fee
INSUFFICIENT_FUNDS409B2C wallet balance below requested amount
KYC_NOT_VERIFIED403KYC must be approved before sending B2C
INVALID_API_KEY401API key missing, invalid, or revoked
RATE_LIMIT_EXCEEDED42960 requests per minute limit exceeded