Search by

cashfin / cashfin-php

Cashfin_Africa

Official PHP SDK for the Cashfin Business API

dev-develop 2026-11-10 13:58 UTC

This package is auto-updated.

Last update: 2026-11-10 12:36:46 UTC


README

Packagist Version PHP Version License

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-json and ext-curl extensions (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:

KeyTypeDefaultDescription
api_keystringrequiredYour Cashfin client secret (cs_…)
timeoutint30Request timeout in seconds
max_retriesint3Retry attempts on transient failures
debugboolfalseLog all requests/responses via error_log()
base_urlstringhttps://api.cashfin.africa/businessOverride 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 message
  • getStatusCode() — HTTP status code
  • getRequestId() — server-side request ID for support tracing
  • getErrorData() — 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.