itamelions / mono-php-sdk
PHP SDK for the Mono open banking API (mandates, accounts, debits, webhooks)
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- mockery/mockery: ^1.6
- phpunit/phpunit: ^10.0
README
Unofficial community PHP SDK for the Mono open banking API.
Supports Mandates, Debits, Customers, Accounts, Banks, and Webhooks.
No framework coupling — works in any PHP 8.1+ project (Laravel, Symfony, plain PHP, etc.).
Requirements
- PHP 8.1 or higher
- Composer
Installation
composer require itamelions/mono-php-sdk
Quick Start
use Mono\Mono; $mono = new Mono($_ENV['MONO_SECRET_KEY']); // Create a customer $customer = $mono->customer()->create([ 'email' => 'john@example.com', 'first_name' => 'John', 'last_name' => 'Doe', 'phone' => '+2348000000000', 'identity' => ['type' => 'bvn', 'number' => '12345678901'], ]); $customerId = $customer['data']['id']; // Initiate a hosted mandate (returns a mono_url — redirect your user there) $initiation = $mono->mandate()->initiate([ 'amount' => 5000000, // amount in kobo (₦50,000) 'type' => 'recurring-debit', 'method' => 'mandate', 'mandate_type' => 'sweep', 'debit_type' => 'variable', 'reference' => 'your-unique-ref', 'redirect_url' => 'https://yourapp.com/mandate/callback', 'customer' => ['id' => $customerId], 'start_date' => '2026-01-01', // Mono interprets bare dates as midnight UTC 'end_date' => '2031-01-01', ]); $monoUrl = $initiation['data']['mono_url']; // → redirect the user to $monoUrl to complete mandate authorisation
Configuration
Set your Mono secret key as an environment variable:
MONO_SECRET_KEY=test_sk_xxxxxxxxxxxxxxxx
Then inject it:
$mono = new Mono(getenv('MONO_SECRET_KEY'));
Resource Reference
Customer
$mono->customer()->create(array $params): array $mono->customer()->update(string $customerId, array $params): array $mono->customer()->fetch(string $customerId): array $mono->customer()->list(array $query = []): array // supports: page, limit
Account
// Exchange a Mono Connect auth code for an account ID (call once after Connect flow) $mono->account()->auth(string $code): array $mono->account()->fetch(string $accountId): array $mono->account()->transactions(string $accountId, int $limit = 100, array $query = []): array $mono->account()->identity(string $accountId): array $mono->account()->income(string $accountId): array $mono->account()->unlink(string $accountId): array
Mandate
// Hosted mandate setup — returns mono_url to redirect user $mono->mandate()->initiate(array $params): array // Direct / e-mandate creation $mono->mandate()->create(array $params): array $mono->mandate()->fetch(string $mandateId): array $mono->mandate()->list(array $query = []): array // supports: page, limit $mono->mandate()->pause(string $mandateId): array $mono->mandate()->reinstate(string $mandateId): array $mono->mandate()->cancel(string $mandateId): array $mono->mandate()->balanceCheck(string $mandateId, ?int $amountInKobo = null): array
E-mandate / Sweep activation delay: Do not call
charge()until you receive theevents.mandates.readywebhook. After a customer approves a sweep or e-mandate, Mono requires up to 3 hours before the mandate is ready to debit. Callingcharge()beforereadyfires will return a Mono API error.Amounts: All
amountvalues must be in the lowest denomination of the account currency — kobo for NGN (100 kobo = ₦1). Pass500000for ₦5,000, not5000.Dates:
start_dateandend_dateacceptY-m-dstrings (e.g.'2026-01-01'). Mono interprets them as midnight UTC. If your application runs in a non-UTC timezone, use UTC-based date logic to avoid off-by-one errors on the mandate start or expiry day.
Debit
// Charge a mandate (required params: amount in kobo, reference, narration) $mono->debit()->charge(string $mandateId, array $params): array $mono->debit()->fetch(string $mandateId, string $reference): array $mono->debit()->all(string $mandateId, array $query = []): array
Bank
$mono->bank()->list(): array
Webhook Verification
Mono signs each webhook request body with your webhook secret using HMAC-SHA512 and sends
the hex digest in the mono-webhook-secret HTTP header.
Basic usage
use Mono\Webhook; use Mono\Exceptions\MonoApiException; $webhook = new Webhook($_ENV['MONO_WEBHOOK_SECRET']); $webhook->on('events.mandates.created', function (array $data) { // persist $data or enqueue a job echo "Mandate created: " . $data['id']; }); $webhook->on('events.mandates.debit.successful', function (array $data) { echo "Debit succeeded: " . $data['reference_number']; }); // Catch-all — receives every event $webhook->on('*', function (string $event, array $data) { error_log("Mono event: {$event}"); }); try { $rawBody = file_get_contents('php://input'); $sigHeader = $_SERVER['HTTP_MONO_WEBHOOK_SECRET'] ?? ''; $webhook->process($rawBody, $sigHeader); http_response_code(200); } catch (MonoApiException $e) { http_response_code($e->getCode() ?: 400); echo $e->getMessage(); }
Manual signature verification
$isValid = $webhook->verifySignature($rawBody, $sigHeader); // bool
Idempotency
Mono may retry unacknowledged webhook deliveries up to 25 times over 48 hours. Always deduplicate on event_id before calling process() to avoid double-processing:
$rawBody = file_get_contents('php://input'); $sigHeader = $_SERVER['HTTP_MONO_WEBHOOK_SECRET'] ?? ''; $preview = json_decode($rawBody, true); $eventId = $preview['event_id'] ?? null; if ($eventId && YourCache::has($eventId)) { http_response_code(200); exit; // already handled } $webhook->process($rawBody, $sigHeader); if ($eventId) { YourCache::put($eventId, true, ttl: 86400); }
Supported webhook events (non-exhaustive)
| Event | Description |
|---|---|
events.mandates.created |
Mandate initiated; awaiting customer approval |
events.mandates.approved |
Customer approved the mandate |
events.mandates.ready |
Mandate ready to debit — wait for this before calling charge() |
events.mandates.rejected |
Mandate was rejected |
events.mandate.action.pause |
Mandate was paused |
events.mandate.action.reinstate |
Paused mandate was reinstated |
events.mandate.action.cancel |
Mandate was cancelled |
events.mandates.expired |
Mandate has passed its end date |
events.mandates.debit.processing |
Debit pending NIBSS confirmation |
events.mandates.debit.successful |
Debit succeeded |
events.mandates.debit.failed |
Debit failed |
Error Handling
All API errors throw subclasses of Mono\Exceptions\MonoApiException:
| Exception | When thrown |
|---|---|
MonoApiException |
Any API error (4xx/5xx) or network failure |
MonoNotFoundException |
API returns 404 Not Found |
use Mono\Exceptions\MonoApiException; use Mono\Exceptions\MonoNotFoundException; try { $mandate = $mono->mandate()->fetch('mmc_invalid'); } catch (MonoNotFoundException $e) { echo "Not found: " . $e->getMessage(); } catch (MonoApiException $e) { echo "API error {$e->getCode()}: " . $e->getMessage(); }
Running Tests
composer install ./vendor/bin/phpunit
Tests use Mockery to mock the Guzzle HTTP client — no live API calls are made.
What This SDK Covers
| Resource | Methods |
|---|---|
| Customer | create, update, fetch, list |
| Account | auth, fetch, transactions, identity, income, unlink |
| Mandate | initiate, create, fetch, list, pause, reinstate, cancel, balanceCheck |
| Debit | charge, fetch, all |
| Bank | list |
| Webhook | process, verifySignature, on() listener |
Version Roadmap
| Version | Scope |
|---|---|
v1.0 |
Accounts, Customers, Mandates, Debits, Banks, Webhooks |
v1.1 |
Laravel service provider + facade |
v1.2 |
Retry middleware, configurable timeout |
v2.0 |
Full Mono Connect (statement, income, identity) |
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Write tests for your changes
- Ensure all tests pass:
./vendor/bin/phpunit - Open a pull request
License
MIT — see LICENSE.
Disclaimer
This is an unofficial community SDK. It is not maintained by or affiliated with Mono. Refer to the official Mono API docs for the authoritative API reference.