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.).
- PHP 8.1 or higher
- Composer
composer require itamelions/mono-php-sdkuse 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 authorisationSet your Mono secret key as an environment variable:
MONO_SECRET_KEY=test_sk_xxxxxxxxxxxxxxxx
Then inject it:
$mono = new Mono(getenv('MONO_SECRET_KEY'));$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// 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// 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): arrayE-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.
// 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$mono->bank()->list(): arrayMono signs each webhook request body with your webhook secret using HMAC-SHA512 and sends
the hex digest in the mono-webhook-secret HTTP header.
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();
}$isValid = $webhook->verifySignature($rawBody, $sigHeader); // boolMono 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);
}| 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 |
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();
}composer install
./vendor/bin/phpunitTests use Mockery to mock the Guzzle HTTP client — no live API calls are made.
| 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 | 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) |
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
MIT — see LICENSE.
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.