A Laravel package that automates the Wave Business dashboard by replaying its internal GraphQL requests — letting you fetch transaction history, send payouts, check balances, and more from your Laravel application without any manual browser interaction.
Note: This package mimics the private HTTP requests made by the
business.wave.comweb application. It is not based on the officialapi.wave.comREST API. Use responsibly and in accordance with Wave's terms of service.
- PHP 8.2+
- Laravel 11, 12, or 13
- A Wave Business account with API/dashboard access
composer require kepson/wave-clientPublish the config file:
php artisan vendor:publish --tag=wave-client-configThis creates config/wave-client.php. Configure your credentials in .env:
# GraphQL endpoint — country-specific prefix (ci = Côte d'Ivoire, sn = Senegal…)
WAVE_API_URL=https://ci.mmapp.wave.com/a/business_graphql
# Public dashboard origin (keep as-is)
WAVE_BASE_URL=https://business.wave.com
# Your Wave Business credentials
WAVE_PHONE=+2250700000000
WAVE_PIN=0000
# A stable UUID identifying this "device" — generate once and keep it fixed
WAVE_DEVICE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Session cache settings (optional)
WAVE_SESSION_STORE=file # any Laravel cache driver
WAVE_SESSION_KEY=wave-client:session
WAVE_SESSION_TTL=3600 # secondsGenerating a device ID:
php artisan tinker --execute "echo \Illuminate\Support\Str::uuid();"Set the output as WAVE_DEVICE_ID and never change it — Wave uses it to recognise your device across sessions.
Wave Business uses a three-step login flow: phone → PIN → SMS code.
use Alal\WaveClient\Exceptions\OtpRequiredException;
try {
Wave::auth()->login(
mobile: '+2250700000000',
pin: '0000',
);
} catch (OtpRequiredException $e) {
// An SMS code has been sent to $e->mobile
// Store $e->tokenId if you need it; the package caches it automatically
echo "Code sent to {$e->mobile}";
}login() always throws OtpRequiredException on success — it is the signal that the SMS was dispatched and you must collect the code.
Wave::auth()->confirmSms('123456');
// Session is now cached — subsequent calls are authenticated automaticallyphp artisan wave:loginThe command prompts for phone and PIN (if not set in .env), then asks for the SMS code interactively.
The authenticated session (sId, walletId, businessId) is stored in your Laravel cache and reused across requests until it expires (WAVE_SESSION_TTL). You do not need to log in on every request.
If the session expires, actions throw AuthenticationException — catch it to trigger a re-login flow:
use Alal\WaveClient\Exceptions\AuthenticationException;
use Alal\WaveClient\Exceptions\OtpRequiredException;
try {
$txns = Wave::transactions()->list();
} catch (AuthenticationException $e) {
// Session expired — re-trigger your login flow
}Wave::auth()->logout(); // clears the cached session// Current month (default)
$transactions = Wave::transactions()->list();
// Custom date range and filters
$transactions = Wave::transactions()->list([
'start' => '2026-06-01',
'end' => '2026-06-30',
'limit' => 50,
'transactionType' => 'ALL', // 'ALL' | 'RECEIVED' | 'SENT'
'searchTerm' => null,
'customerMobileStr'=> '+2250700000000',
'includePending' => true,
]);
// Each item is a TransactionData object
foreach ($transactions as $txn) {
echo $txn->type; // e.g. MerchantSaleEntry, PayoutTransferEntry…
echo $txn->amount; // e.g. "5000"
echo $txn->summary; // human-readable label
echo $txn->date; // whenEntered ISO 8601
echo $txn->mobile; // counterparty phone (normalised across types)
echo $txn->name; // counterparty name
echo $txn->isPending; // bool
echo $txn->isCancelled; // bool
echo $txn->grossAmount; // before fees (MerchantSaleEntry, PayoutTransferEntry)
echo $txn->feeAmount; // fee charged
echo $txn->clientReference; // your reference (MerchantSaleEntry)
// $txn->raw — full raw GraphQL response array for this entry
}
// Find a single transaction by ID
$txn = Wave::transactions()->find('TXN_ID');$balance = Wave::balance()->get();
echo $balance->amount; // e.g. "125000"
echo $balance->currency; // e.g. "XOF"$payout = Wave::payout()->send(
mobile: '+2250700000000',
amount: '5000',
options: [
'name' => 'John Doe',
'reference' => 'INV-2026-001',
'reason' => 'Salary June',
]
);
echo $payout->id;
echo $payout->status; // processing | succeeded | failed$request = Wave::paymentRequest()->create([
'amount' => '10000',
'currency' => 'XOF',
'reference' => 'ORDER-42',
'description' => 'Payment for order #42',
]);
echo $request->paymentUrl; // share this link with your customer
echo $request->status; // pending | completed | cancelled| Exception | When thrown |
|---|---|
OtpRequiredException |
login() succeeded — SMS was sent. Carry on with confirmSms(). |
AuthenticationException |
No valid session exists, or the session expired. Re-run the login flow. |
WaveRequestException |
The HTTP request failed (non-2xx response or GraphQL error field). |
RateLimitException |
HTTP 429 — too many requests. Check $e->getMessage() for retry hint. |
All exceptions extend WaveRequestException, which exposes the raw $e->response (an Illuminate\Http\Client\Response) for debugging.
The package ships a WaveFake that swaps out the real client in tests — no network calls, no OTP, no credentials needed.
use Alal\WaveClient\Facades\Wave;
// In your test
$fake = Wave::fake();$fake->queueBalance([
'balance' => '50000',
'currency' => 'XOF',
]);
$fake->queuePayout([
'id' => 'payout-001',
'status' => 'processing',
'amount' => '5000',
'currency' => 'XOF',
'mobile' => '+2250700000000',
]);
$fake->queueTransactions([
[
'id' => 'txn-001',
'__typename' => 'MerchantSaleEntry',
'amount' => '3000',
'summary' => 'Sale',
'whenEntered' => '2026-06-10T14:00:00Z',
'isPending' => false,
'isCancelled' => false,
],
]);
$fake->queuePaymentRequest([
'id' => 'pr-001',
'status' => 'pending',
'amount' => '10000',
'currency' => 'XOF',
'payment_url' => 'https://pay.wave.com/pr-001',
]);If no response is queued for an action, the fake returns a sensible default.
// Assert an action was called (any args)
$fake->assertSent('balance.get');
$fake->assertSent('payout.send');
$fake->assertSent('transactions.list');
$fake->assertSent('paymentRequest.create');
// Assert with argument inspection
$fake->assertSent('payout.send', fn(array $args) =>
$args['mobile'] === '+2250700000000' &&
$args['amount'] === '5000'
);
$fake->assertSent('auth.login', fn(array $args) =>
$args['phone'] === '+2250700000000'
);
// Assert an action was NOT called
$fake->assertNotSent('payout.send');
// Assert nothing at all was sent
$fake->assertNothingSent();it('sends a payout when a payment is approved', function () {
$fake = Wave::fake();
$fake->queuePayout([
'id' => 'p-1',
'status' => 'processing',
'amount' => '5000',
'currency' => 'XOF',
'mobile' => '+2250700000000',
]);
// Call the service that internally uses Wave::payout()->send(...)
app(PaymentService::class)->disburse($payment);
$fake->assertSent('payout.send', fn(array $args) =>
$args['mobile'] === '+2250700000000'
);
});public function test_balance_is_fetched_on_dashboard(): void
{
$fake = Wave::fake();
$fake->queueBalance(['balance' => '25000', 'currency' => 'XOF']);
$response = $this->get('/dashboard');
$response->assertSee('25 000 XOF');
$fake->assertSent('balance.get');
}| Key | Triggered by |
|---|---|
auth.login |
Wave::auth()->login() |
auth.confirmSms |
Wave::auth()->confirmSms() |
auth.logout |
Wave::auth()->logout() |
balance.get |
Wave::balance()->get() |
payout.send |
Wave::payout()->send() |
payout.find |
Wave::payout()->find() |
payout.search |
Wave::payout()->search() |
transactions.list |
Wave::transactions()->list() |
transactions.find |
Wave::transactions()->find() |
paymentRequest.create |
Wave::paymentRequest()->create() |
paymentRequest.find |
Wave::paymentRequest()->find() |
paymentRequest.cancel |
Wave::paymentRequest()->cancel() |
- Open DevTools → Network → copy the request as cURL.
- Identify the GraphQL query string and variables.
- Add a
const QUERYand implement the method in the relevantsrc/Actions/class. - Map the response fields in the corresponding
src/Data/DTO. - Add a
queue*helper and tests inWaveFakeif needed.
The WaveConnector::graphql() method handles auth, headers, error mapping, and trace context automatically.
cd wave-client
composer install
composer testMIT — see LICENSE.