PHP SDK
Copy
Ask AI
composer require palpluss/sdk
ext-json extension (standard in all modern PHP distributions).
Client initialisation
Copy
Ask AI
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
- Laravel
- Symfony
- Plain PHP
Register as a singleton in a service provider:Add the key in ControllerWebhook controllerRoutesQueue jobs for async processingDispatching a job from the webhook endpoint keeps response times fast and prevents timeout retries:
Copy
Ask AI
// 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'),
);
});
}
}
config/services.php:Copy
Ask AI
'palpluss' => [
'key' => env('PALPLUSS_API_KEY'),
],
Copy
Ask AI
// 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);
}
}
}
Copy
Ask AI
// 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'],
]);
}
}
Copy
Ask AI
// 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]);
Copy
Ask AI
// 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
}
}
}
Copy
Ask AI
// In WebhookController::handlePalPluss — dispatch instead of processing inline
ProcessPalPlussWebhook::dispatch($payload);
return response('', 200);
Register the client as a service:ControllerWebhook controller
Copy
Ask AI
# config/services.yaml
services:
PalPluss\PalPluss:
arguments:
$apiKey: '%env(PALPLUSS_API_KEY)%'
Copy
Ask AI
// src/Controller/PaymentController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use PalPluss\PalPluss;
use PalPluss\Http\Errors\PalPlussApiError;
class PaymentController extends AbstractController
{
public function __construct(private readonly PalPluss $palpluss) {}
#[Route('/api/pay', methods: ['POST'])]
public function initiatePayment(Request $request): JsonResponse
{
$body = json_decode($request->getContent(), true);
try {
$tx = $this->palpluss->stkPush(
amount: $body['amount'],
phone: $body['phone'],
accountReference: $body['reference'] ?? '',
callbackUrl: $this->generateUrl(
'webhook_palpluss',
referenceType: \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL
),
);
return $this->json([
'transaction_id' => $tx['transactionId'],
'status' => $tx['status'],
]);
} catch (PalPlussApiError $e) {
return $this->json(
['error' => $e->getMessage(), 'code' => $e->errorCode],
$e->httpStatus,
);
}
}
}
Copy
Ask AI
// src/Controller/WebhookController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use PalPluss\Webhooks;
class WebhookController extends AbstractController
{
#[Route('/webhooks/palpluss', name: 'webhook_palpluss', methods: ['POST'])]
public function handlePalPluss(Request $request): Response
{
try {
$payload = Webhooks::parsePayload($request->getContent());
} catch (\InvalidArgumentException) {
return new Response('Invalid payload', Response::HTTP_BAD_REQUEST);
}
$eventType = $payload['event_type'];
$transaction = $payload['transaction'];
if ($eventType === 'transaction.success') {
// fulfil order
} elseif ($eventType === 'transaction.failed') {
// notify customer
}
return new Response('', Response::HTTP_OK);
}
}
Copy
Ask AI
<?php
// index.php
require_once __DIR__ . '/vendor/autoload.php';
use PalPluss\PalPluss;
use PalPluss\Webhooks;
use PalPluss\Http\Errors\PalPlussApiError;
use PalPluss\Http\Errors\RateLimitError;
$palpluss = new PalPluss(apiKey: $_ENV['PALPLUSS_API_KEY'] ?? getenv('PALPLUSS_API_KEY'));
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// POST /pay
if ($method === 'POST' && $path === '/pay') {
$body = json_decode(file_get_contents('php://input'), true);
try {
$tx = $palpluss->stkPush(
amount: (float) $body['amount'],
phone: $body['phone'],
accountReference: $body['reference'] ?? '',
callbackUrl: 'https://yourapp.com/webhooks/palpluss',
);
header('Content-Type: application/json');
echo json_encode(['transaction_id' => $tx['transactionId'], 'status' => $tx['status']]);
} catch (RateLimitError $e) {
http_response_code(429);
header("Retry-After: {$e->retryAfter}");
echo json_encode(['error' => 'Rate limited', 'retry_after' => $e->retryAfter]);
} catch (PalPlussApiError $e) {
http_response_code($e->httpStatus);
echo json_encode(['error' => $e->getMessage(), 'code' => $e->errorCode]);
}
exit;
}
// POST /webhooks/palpluss
if ($method === 'POST' && $path === '/webhooks/palpluss') {
$raw = file_get_contents('php://input');
try {
$payload = Webhooks::parsePayload($raw);
} catch (\InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => 'Invalid payload']);
exit;
}
http_response_code(200);
echo json_encode(['received' => true]);
// Process after sending response
fastcgi_finish_request(); // only on PHP-FPM
$eventType = $payload['event_type'];
$transaction = $payload['transaction'];
if ($eventType === 'transaction.success') {
// fulfil order
}
exit;
}
http_response_code(404);
echo json_encode(['error' => 'Not found']);
STK Push
Copy
Ask AI
$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 anidempotencyKey so you can safely retry without double-paying:
Copy
Ask AI
$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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
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
| Code | HTTP | Meaning |
|---|---|---|
INVALID_PHONE | 400 | Phone number format not recognised |
INSUFFICIENT_SERVICE_BALANCE | 402 | Not enough service tokens to cover the fee |
INSUFFICIENT_FUNDS | 409 | B2C wallet balance below requested amount |
KYC_NOT_VERIFIED | 403 | KYC must be approved before sending B2C |
INVALID_API_KEY | 401 | API key missing, invalid, or revoked |
RATE_LIMIT_EXCEEDED | 429 | 60 requests per minute limit exceeded |