cashfin / cashfin-php
Official PHP SDK for the Cashfin Business API
Requires
- php: ^8.0
- ext-curl: *
- ext-json: *
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^10.0
- squizlabs/php_codesniffer: ^3.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-10 12:36:46 UTC
README
Official PHP SDK for the Cashfin Business API. Works with plain PHP, Laravel, Symfony, and any other PHP framework.
Requirements
- PHP 8.0 or later
- Composer
ext-jsonandext-curlextensions (standard in most environments)
Installation
composer require cashfin/cashfin-php
Quick Start
<?php
require 'vendor/autoload.php';
use Cashfin\CashfinClient;
$cashfin = new CashfinClient([
'api_key' => 'cs_your_client_secret',
]);
// List products
$result = $cashfin->products->all(['limit' => 10]);
foreach ($result['data'] as $product) {
echo $product['title'] . ' — KES ' . $product['price'] . PHP_EOL;
}
// Initiate M-Pesa payment
$payment = $cashfin->payments->mpesa([
'amount' => 1500,
'phone' => '254712345678',
'referenceid' => 'ORDER-001',
]);
echo 'Checkout ID: ' . $payment['data']['checkoutrequestid'];
Configuration
Pass a configuration array to CashfinClient:
| Key | Type | Default | Description |
|---|---|---|---|
api_key | string | required | Your Cashfin client secret (cs_…) |
timeout | int | 30 | Request timeout in seconds |
max_retries | int | 3 | Retry attempts on transient failures |
debug | bool | false | Log all requests/responses via error_log() |
base_url | string | https://api.cashfin.africa/business | Override the base URL |
$cashfin = new CashfinClient([
'api_key' => getenv('CASHFIN_API_KEY'),
'timeout' => 60,
'max_retries' => 5,
'debug' => true,
]);
Framework Integration
Laravel
The SDK auto-discovers the service provider. After installing, publish the config:
php artisan vendor:publish --tag=cashfin-config
Add your key to .env:
CASHFIN_API_KEY=cs_your_client_secret
Dependency injection:
use Cashfin\CashfinClient;
class PaymentController extends Controller
{
public function __construct(private CashfinClient $cashfin) {}
public function pay(Request $request)
{
$payment = $this->cashfin->payments->mpesa([
'amount' => $request->amount,
'phone' => $request->phone,
]);
return response()->json($payment);
}
}
Facade:
use Cashfin\Laravel\Facades\Cashfin;
$products = Cashfin::products()->all();
Symfony
Register the client as a service in config/services.yaml:
services:
Cashfin\CashfinClient:
arguments:
- api_key: '%env(CASHFIN_API_KEY)%'
timeout: 30
max_retries: 3
Inject it into your controller or service normally via constructor injection.
Plain PHP
require 'vendor/autoload.php';
$cashfin = new \Cashfin\CashfinClient(['api_key' => 'cs_xxxx']);
API Reference
All methods return plain PHP arrays matching the API response shape. Paginated endpoints return:
[
'success' => true,
'data' => [ /* array of items */ ],
'meta' => [
'page' => 1,
'limit' => 10,
'total' => 42,
'pages' => 5,
'hasNext' => true,
'hasPrev' => false,
],
]
Products — $cashfin->products
// List products
$cashfin->products->all([
'page' => 1,
'limit' => 20,
'status' => 'published', // draft | published | archived
'type' => 'product', // product | service | plan
'categoryid' => 'abc123',
'featured' => true,
]);
// Search the catalogue the way the storefront does: title, SKU or
// description, optionally within a price range, in-stock products first
$cashfin->products->all([
'search' => 'running shoe',
'minprice' => 1000,
'maxprice' => 5000,
'stockfirst' => true,
]);
// Get a product
$cashfin->products->retrieve('507f1f77bcf86cd799439011');
// Create a product
$cashfin->products->create([
'title' => 'Premium Widget',
'price' => 1999.99,
'description' => 'A high-quality widget',
'stock' => 100,
'sku' => 'WDGT-001',
'type' => 'product',
'status' => 'published',
'variants' => [
['attributetitle' => 'Color', 'valuetitle' => 'Red', 'valueprice' => 2099.99],
],
]);
// Update a product (partial: only the keys you pass change; 0 and '' are honoured)
$cashfin->products->update('507f1f77bcf86cd799439011', [
'description' => 'A high-quality widget',
'stock' => 0, // sold out
]);
// Turn inventory tracking on or off (off = unlimited stock, never reserved)
$cashfin->products->update('507f1f77bcf86cd799439011', ['stocktracked' => true]);
// Delete a product (soft: marked inactive, hidden from all()/retrieve(), history kept)
$cashfin->products->delete('507f1f77bcf86cd799439011');
// Restore a deleted product
$cashfin->products->update('507f1f77bcf86cd799439011', ['active' => true]);
Sales and "was" prices
On every read, price is what the customer pays now and compareprice is the
struck-through price to show beside it (0 when there is none). While a sale
runs, onsale is true, listprice is the price before the sale and sale
carries its label, startat and endat. Variants carry valueprice,
valuelistprice, valuecompareprice and valueonsale.
// 'ruletype' is 'price' (rulevalue is the sale price), 'percentage' or
// 'amount' (that much off). Cashfin derives the sale price, rounded to the
// business currency; it must be below the list price.
$cashfin->products->update('507f1f77bcf86cd799439011', [
'sale' => [
'ruletype' => 'percentage',
'rulevalue' => 20,
'startat' => '2026-11-27T00:00:00+03:00', // optional
'endat' => '2026-11-30T23:59:59+03:00', // optional
'label' => 'Black Friday', // optional, <= 40 chars
],
]);
// End the sale now (an explicit null; leaving 'sale' out keeps it)
$cashfin->products->update('507f1f77bcf86cd799439011', ['sale' => null]);
// A manual "was" price with no sale; 0 clears it
$cashfin->products->update('507f1f77bcf86cd799439011', ['compareatprice' => 3000]);
// Always write the LIST price: while on sale, 'price' on a read is the sale price
$p = $cashfin->products->retrieve('507f1f77bcf86cd799439011');
$cashfin->products->update($p['id'], ['price' => ($p['listprice'] ?? $p['price']) + 100]);
Categories — $cashfin->categories
$cashfin->categories->all(['status' => 'active']);
$cashfin->categories->create(['title' => 'Electronics', 'status' => 'active']);
$cashfin->categories->update($id, ['title' => 'Updated Name']);
Sales (bulk) — $cashfin->sales
A sale campaign marks down many products at once: the whole catalogue, chosen
products, categories (descendants included) or brands. Cashfin derives each
product's sale price from its own list price, rounded to the business
currency, and starts and ends the campaign on its clock. While it runs, every
product in it reads like a product on sale: price is the sale price,
listprice the price before it, compareprice the struck-through price and
onsale is true. Order lines bought under it carry salecampaignid, with
listrate and salediscount beside the rate charged.
// 20% off a category for a week
$sale = $cashfin->sales->create([
'title' => 'Shoe week',
'ruletype' => 'percentage', // or 'amount': rulevalue off each price
'rulevalue' => 20,
'scope' => ['kind' => 'categories', 'ids' => ['category-id']], // or products, brands, all
'excludeproductids' => ['product-id'], // optional, at most 500
'label' => 'Shoe Week', // optional badge, <= 40 chars
'startat' => '2026-11-23T00:00:00+03:00', // omit to start now
'endat' => '2026-11-30T00:00:00+03:00', // omit to run until ended
]);
echo $sale['status'], ' ', $sale['productcount']; // scheduled 42
// Campaigns, newest first
$sales = $cashfin->sales->all(['status' => 'active']);
$sale = $cashfin->sales->retrieve($sale['id']);
// Rename, relabel, shorten or extend. The scope and rule are fixed; create a
// new campaign to change them.
$cashfin->sales->update($sale['id'], ['endat' => '2026-12-07T00:00:00+03:00']);
// End it early: its products go back to their list price at once
$cashfin->sales->end($sale['id']);
// Cancel and remove it from all()
$cashfin->sales->delete($sale['id']);
A manual sale set with products->update() is never overridden by a
campaign, and when two campaigns cover one product the cheaper price wins.
Orders — $cashfin->orders
$cashfin->orders->all(['status' => 'pending']);
$cashfin->orders->retrieve($id);
// Create / checkout
$cashfin->orders->checkout([
'customeremail' => 'john@example.com',
'items' => [
['itemid' => 'prod_abc', 'quantity' => 2, 'rate' => 1999.99],
],
'shippingaddress' => [
'name' => 'John Doe',
'address' => '123 Main St',
'city' => 'Nairobi',
'country' => 'KE',
'phone' => '254712345678',
],
]);
Payments — $cashfin->payments
// M-Pesa STK Push
$payment = $cashfin->payments->mpesa([
'amount' => 1500, // KES
'phone' => '254712345678', // 254XXXXXXXXX format
'referenceid' => 'ORDER-001',
'description' => 'Payment for Order #001',
]);
// $payment['data']['checkoutrequestid'] — track via webhook
// Payment methods, as the Cashfin checkout lists them
// (methods switched off on the Integrations page are left out;
// currency is optional and defaults to your base currency)
$available = $cashfin->payments->methods(['currency' => 'KES']);
foreach ($available['options'] as $option) {
// ['id' => 'mobile', 'name' => 'Mobile Money', 'channels' => ['mpesa']]
echo $option['name'];
}
// $available['methods'] — raw rows: title, channel, merchanttype, currency, bankdetails
Customers — $cashfin->customers
$cashfin->customers->all(['type' => 'individual']);
$cashfin->customers->retrieve($id);
$cashfin->customers->create([
'name' => 'Jane Doe',
'email' => 'jane@example.com',
'phone' => '254700000000',
'country' => 'KE',
'currency' => 'KES',
'type' => 'individual',
]);
$cashfin->customers->update($id, ['phone' => '254711111111']);
Invoices — $cashfin->invoices
$cashfin->invoices->all(['status' => 'sent', 'customerid' => $customerId]);
$cashfin->invoices->retrieve($id);
$cashfin->invoices->create([
'customerid' => $customerId,
'items' => [['name' => 'Consulting', 'quantity' => 5, 'rate' => 1000]],
'duedate' => '2025-12-31',
]);
$cashfin->invoices->update($id, ['status' => 'paid']);
Subscriptions — $cashfin->subscriptions
$cashfin->subscriptions->all(['status' => 'active']);
$cashfin->subscriptions->retrieve($id);
$cashfin->subscriptions->create([
'customerid' => $customerId,
'items' => [['name' => 'Pro Plan', 'rate' => 2999]],
'billingcycle' => 'monthly', // daily | weekly | monthly | quarterly | yearly
'autorenew' => true,
]);
Payment Links — $cashfin->paymentLinks
$cashfin->paymentLinks->all(['status' => 'active']);
$cashfin->paymentLinks->retrieve($id);
$cashfin->paymentLinks->create([
'title' => 'Pay for Event Ticket',
'amount' => 500,
'description' => 'Annual conference ticket',
'currency' => 'KES',
]);
Transactions — $cashfin->transactions
$cashfin->transactions->all(['status' => 'completed', 'method' => 'mpesa']);
$cashfin->transactions->retrieve($id);
Receipts — $cashfin->receipts
$cashfin->receipts->all(['customerid' => $customerId]);
$cashfin->receipts->retrieve($id);
Vendors — $cashfin->vendors
$cashfin->vendors->all();
$cashfin->vendors->retrieve($id);
$cashfin->vendors->create(['name' => 'Acme Supplies', 'email' => 'acme@example.com']);
$cashfin->vendors->update($id, ['phone' => '254700000001']);
Expenses — $cashfin->expenses
$cashfin->expenses->all(['status' => 'pending', 'vendorid' => $vendorId]);
$cashfin->expenses->retrieve($id);
$cashfin->expenses->create([
'title' => 'Office Supplies',
'amount' => 3500,
'category' => 'supplies',
'date' => '2025-06-01',
'vendorid' => $vendorId,
]);
Bills — $cashfin->bills
$cashfin->bills->all(['status' => 'pending', 'vendorid' => $vendorId]);
$cashfin->bills->retrieve($id);
$cashfin->bills->create([
'vendorid' => $vendorId,
'items' => [['name' => 'Monthly Rent', 'quantity' => 1, 'rate' => 50000]],
'duedate' => '2025-07-01',
]);
Purchase Orders — $cashfin->purchaseOrders
$cashfin->purchaseOrders->all(['status' => 'draft', 'vendorid' => $vendorId]);
$cashfin->purchaseOrders->retrieve($id);
$cashfin->purchaseOrders->create([
'vendorid' => $vendorId,
'items' => [['name' => 'Printer Paper', 'quantity' => 10, 'rate' => 500]],
]);
Leads — $cashfin->leads
$cashfin->leads->all(['status' => 'new']);
$cashfin->leads->retrieve($id);
$cashfin->leads->create([
'name' => 'Prospect Ltd',
'email' => 'contact@prospect.co.ke',
'phone' => '254700000002',
'source' => 'website',
'budget' => 100000,
]);
$cashfin->leads->update($id, ['status' => 'qualified']);
Quotes — $cashfin->quotes
$cashfin->quotes->all(['status' => 'sent', 'customerid' => $customerId]);
$cashfin->quotes->retrieve($id);
Contacts — $cashfin->contacts
$cashfin->contacts->all();
$cashfin->contacts->retrieve($id);
$cashfin->contacts->create(['name' => 'Alice Smith', 'email' => 'alice@example.com']);
$cashfin->contacts->update($id, ['position' => 'CEO']);
Contracts — $cashfin->contracts
$cashfin->contracts->all(['status' => 'active']);
$cashfin->contracts->retrieve($id);
Bookings — $cashfin->bookings
$cashfin->bookings->all(['status' => 'scheduled']);
$cashfin->bookings->retrieve($id);
Appointments — $cashfin->appointments
$cashfin->appointments->all(['status' => 'active']);
$cashfin->appointments->retrieve($id);
Campaigns — $cashfin->campaigns
$cashfin->campaigns->all(['status' => 'sent', 'type' => 'email']);
$cashfin->campaigns->retrieve($id);
Marketing Lists — $cashfin->lists
$cashfin->lists->all(['type' => 'email']);
$cashfin->lists->retrieve($id);
$cashfin->lists->create(['name' => 'Newsletter Subscribers', 'type' => 'email']);
// Add a contact to a list
$cashfin->lists->addContact($listId, [
'email' => 'subscriber@example.com',
'firstname' => 'Bob',
'lastname' => 'Kariuki',
]);
Error Handling
The SDK throws typed exceptions for every API error. Catch the specific exception you expect or the base CashfinException as a fallback.
use Cashfin\Exceptions\AuthenticationException;
use Cashfin\Exceptions\ValidationException;
use Cashfin\Exceptions\NotFoundException;
use Cashfin\Exceptions\RateLimitException;
use Cashfin\Exceptions\ConflictException;
use Cashfin\Exceptions\ForbiddenException;
use Cashfin\Exceptions\ServerException;
use Cashfin\Exceptions\NetworkException;
use Cashfin\Exceptions\CashfinException;
try {
$product = $cashfin->products->create([
'title' => 'New Product',
'price' => 999,
]);
} catch (ValidationException $e) {
// 422 — field-level errors
foreach ($e->getErrors() as $field => $message) {
echo "{$field}: {$message}\n";
}
} catch (AuthenticationException $e) {
// 401 — invalid API key
echo 'Check your CASHFIN_API_KEY.';
} catch (NotFoundException $e) {
// 404 — resource doesn't exist
echo 'Not found: ' . $e->getMessage();
} catch (RateLimitException $e) {
// 429 — retry after $e->getRetryAfter() seconds
sleep($e->getRetryAfter());
} catch (ConflictException $e) {
// 409 — duplicate resource
} catch (ForbiddenException $e) {
// 403 — insufficient permissions
} catch (ServerException $e) {
// 500/502/503/504
} catch (NetworkException $e) {
// Connection failed
} catch (CashfinException $e) {
// Catch-all for any other Cashfin error
echo $e->getMessage() . ' (HTTP ' . $e->getStatusCode() . ')';
}
All exceptions expose:
getMessage()— human-readable error messagegetStatusCode()— HTTP status codegetRequestId()— server-side request ID for support tracinggetErrorData()— raw response body as array
Webhooks
Cashfin sends webhook events to your endpoint when payment, order, or subscription events occur.
<?php
require 'vendor/autoload.php';
use Cashfin\Webhook;
use Cashfin\Exceptions\CashfinException;
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CASHFIN_SIGNATURE'] ?? '';
$secret = getenv('CASHFIN_WEBHOOK_SECRET');
try {
$event = Webhook::constructEvent($payload, $signature, $secret);
} catch (CashfinException $e) {
http_response_code(400);
exit($e->getMessage());
}
switch ($event['type']) {
case 'payment.completed':
$data = $event['data'];
// fulfil the order using $data['transactionid'], $data['amount'], etc.
break;
case 'order.created':
// process the new order
break;
case 'invoice.paid':
// mark invoice as settled in your system
break;
case 'subscription.renewed':
// extend the customer's access
break;
}
http_response_code(200);
echo json_encode(['received' => true]);
Laravel webhook route example:
// routes/api.php
Route::post('/webhooks/cashfin', [WebhookController::class, 'handle'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
// app/Http/Controllers/WebhookController.php
use Cashfin\Webhook;
class WebhookController extends Controller
{
public function handle(Request $request)
{
$event = Webhook::constructEvent(
$request->getContent(),
$request->header('X-Cashfin-Signature', ''),
config('cashfin.webhook_secret')
);
// Handle $event['type'] …
return response()->json(['received' => true]);
}
}
Pagination
All list methods accept page and limit parameters. The response includes a meta key:
$page = 1;
do {
$result = $cashfin->products->all(['page' => $page, 'limit' => 50]);
foreach ($result['data'] as $product) {
// process $product
}
$page++;
} while ($result['meta']['hasNext']);
Debugging
Set debug => true in the config (or CASHFIN_DEBUG=true for Laravel) to log all requests and raw responses to PHP's error log:
[Cashfin] [POST] /payments/mobile/request
[Cashfin] Response 200 {"success":true,...}
Versioning
This SDK follows Semantic Versioning:
- PATCH (
1.0.x) — bug fixes, no breaking changes - MINOR (
1.x.0) — new features, backwards-compatible - MAJOR (
x.0.0) — breaking changes
Check CHANGELOG.md for the full history.
Contributing
Pull requests are welcome. Please run the test suite before submitting:
composer install
composer test
Support
- Documentation: docs.cashfin.africa
- Email: support@cashfin.africa
- Issues: open a ticket in your Cashfin dashboard
License
MIT — see LICENSE.