PHPackages                             kepson/wave-client - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. kepson/wave-client

ActiveLibrary[HTTP &amp; Networking](/categories/http)

kepson/wave-client
==================

Laravel client that automates the Wave Business web dashboard by replaying its internal HTTP requests (session + OTP based).

v1.0.0(1mo ago)2281MITPHPPHP ^8.2

Since Jun 19Pushed 2w agoCompare

[ Source](https://github.com/Kepsondiaz/wave-client)[ Packagist](https://packagist.org/packages/kepson/wave-client)[ RSS](/packages/kepson-wave-client/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (5)Versions (2)Used By (0)

wave-client
===========

[](#wave-client)

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.com` web application. It is not based on the official `api.wave.com` REST API. Use responsibly and in accordance with Wave's terms of service.

---

Requirements
------------

[](#requirements)

- PHP **8.2+**
- Laravel **11, 12, or 13**
- A **Wave Business** account with API/dashboard access

---

Installation
------------

[](#installation)

### From Packagist (once published)

[](#from-packagist-once-published)

```
composer require kepson/wave-client
```

Configuration
-------------

[](#configuration)

Publish the config file:

```
php artisan vendor:publish --tag=wave-client-config
```

This 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       # seconds
```

**Generating 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.

---

Authentication
--------------

[](#authentication)

Wave Business uses a **three-step login flow**: phone → PIN → SMS code.

### Step 1 — Initiate login

[](#step-1--initiate-login)

```
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.

### Step 2 — Confirm the SMS code

[](#step-2--confirm-the-sms-code)

```
Wave::auth()->confirmSms('123456');
// Session is now cached — subsequent calls are authenticated automatically
```

### Via Artisan (interactive terminal)

[](#via-artisan-interactive-terminal)

```
php artisan wave:login
```

The command prompts for phone and PIN (if not set in `.env`), then asks for the SMS code interactively.

### Session persistence

[](#session-persistence)

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
}
```

### Logout

[](#logout)

```
Wave::auth()->logout(); // clears the cached session
```

---

Usage
-----

[](#usage)

### Transactions

[](#transactions)

```
// 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 *(stub — wire your captured request)*

[](#balance-stub--wire-your-captured-request)

```
$balance = Wave::balance()->get();

echo $balance->amount;   // e.g. "125000"
echo $balance->currency; // e.g. "XOF"
```

### Payout *(stub — wire your captured request)*

[](#payout-stub--wire-your-captured-request)

```
$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
```

### Payment Request *(stub — wire your captured request)*

[](#payment-request-stub--wire-your-captured-request)

```
$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 Reference
-------------------

[](#exception-reference)

ExceptionWhen 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.

---

Testing
-------

[](#testing)

The package ships a `WaveFake` that swaps out the real client in tests — no network calls, no OTP, no credentials needed.

### Basic usage

[](#basic-usage)

```
use Alal\WaveClient\Facades\Wave;

// In your test
$fake = Wave::fake();
```

### Queueing canned responses

[](#queueing-canned-responses)

```
$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.

### Assertions

[](#assertions)

```
// 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();
```

### Full test example (Pest)

[](#full-test-example-pest)

```
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'
    );
});
```

### Full test example (PHPUnit)

[](#full-test-example-phpunit)

```
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');
}
```

---

Available action keys for assertions
------------------------------------

[](#available-action-keys-for-assertions)

KeyTriggered 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()`---

Wiring a new action from a captured request
-------------------------------------------

[](#wiring-a-new-action-from-a-captured-request)

1. Open DevTools → Network → copy the request as cURL.
2. Identify the GraphQL query string and variables.
3. Add a `const QUERY` and implement the method in the relevant `src/Actions/` class.
4. Map the response fields in the corresponding `src/Data/` DTO.
5. Add a `queue*` helper and tests in `WaveFake` if needed.

The `WaveConnector::graphql()` method handles auth, headers, error mapping, and trace context automatically.

---

Running the package tests
-------------------------

[](#running-the-package-tests)

```
cd wave-client
composer install
composer test
```

---

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance94

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 85.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

42d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/67605105?v=4)[Kepson Diaz](/maintainers/Kepsondiaz)[@Kepsondiaz](https://github.com/Kepsondiaz)

---

Top Contributors

[![Kepsondiaz](https://avatars.githubusercontent.com/u/67605105?v=4)](https://github.com/Kepsondiaz "Kepsondiaz (6 commits)")[![nosleepman1](https://avatars.githubusercontent.com/u/212015359?v=4)](https://github.com/nosleepman1 "nosleepman1 (1 commits)")

---

Tags

laravelbusinesshttp clientpaymentswavewave-money

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/kepson-wave-client/health.svg)

```
[![Health](https://phpackages.com/badges/kepson-wave-client/health.svg)](https://phpackages.com/packages/kepson-wave-client)
```

###  Alternatives

[laravel/socialite

Laravel wrapper around OAuth 1 &amp; OAuth 2 libraries.

5.7k108.5M934](/packages/laravel-socialite)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M348](/packages/psalm-plugin-laravel)[spatie/laravel-export

Create a static site bundle from a Laravel app

674146.0k6](/packages/spatie-laravel-export)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1235.9k21](/packages/fleetbase-core-api)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
