PHP
The wajub/wajub-php package, its resources, and how it fits a Laravel app.
wajub/wajub-php is your server's Wajub client: with your secret key, it creates payments, reads
their status and verifies webhook signatures, with or without a framework.
wajub/wajub-php
Stable · GAPackagist
- Version
- 2.0.1
- Runtime
- PHP 8.4+, Guzzle 8.2+, ext-json
Covers
- Payments
- Billing
- Transfers
- Sync
- Shield
- Tax
Install
composer require wajub/wajub-phpCreate the client
The constructor takes an array. api_key has no environment fallback, so read the variable
yourself. Only webhook_secret falls back to WAJUB_WEBHOOK_SECRET when omitted.
<?php
use Wajub\Wajub;
$wajub = new Wajub([
'api_key' => getenv('WAJUB_API_KEY'),
'webhook_secret' => getenv('WAJUB_WEBHOOK_SECRET'),
]);| Key | What it does |
|---|---|
api_key | Your sk. or sk_test. key. Required, an empty value throws |
webhook_secret | The whsec_ secret, needed only for webhooks->constructEvent(). Defaults to WAJUB_WEBHOOK_SECRET |
http_client | Your own GuzzleHttp\Client, for a proxy or a mocked handler in tests. Give a real one base_uri https://api.wajub.com and http_errors false |
max_network_retries | How many times a temporary failure is retried |
timeout | Request timeout in seconds, when you pass no http_client |
In a Laravel application
Bind it once as a singleton and inject it.
<?php
// app/Providers/AppServiceProvider.php
use Wajub\Wajub;
public function register(): void
{
$this->app->singleton(Wajub::class, fn () => new Wajub([
'api_key' => config('services.wajub.secret'),
'webhook_secret' => config('services.wajub.webhook_secret'),
]));
}In Laravel, add the two keys to config/services.php rather than calling env() outside a config
file, or php artisan config:cache will hand you a null key in production.
'wajub' => [
'secret' => env('WAJUB_API_KEY'),
'webhook_secret' => env('WAJUB_WEBHOOK_SECRET'),
],The first call
$payment = $wajub->payments->create([
'amount' => 25000,
'currency' => 'XAF',
'email' => 'amina@example.com',
'description' => 'Order 4172',
'reference' => 'order-4172',
'callback' => route('checkout.complete'),
]);
return redirect()->away($payment->authorization_url);Parameters go in as an associative array with the API's exact field names. What comes back is an
ApiObject, so every field is a property and nothing is lost.
$payment->id; // trx_test_8kQ2mW9vB4nL6hR1cY3d
$payment->status; // pending
$payment->authorization_url; // https://pay.wajub.com/test_xxxxx
$payment->authorization_token; // test_xxxxxAmounts are in the major unit
25000 with XAF is twenty-five thousand francs. A decimal currency takes a decimal:
'amount' => 12.50 with GHS.
Every resource on the client
| Property | Methods |
|---|---|
| $wajub->global | ping, channels, countries, currencies |
| $wajub->payments | create, retrieve, list, cancel, process, processSplit, listRefunds |
| $wajub->customers | create, retrieve, update, delete, list, block, unblock, activate, deactivate, listTaxIds, createTaxId, deleteTaxId |
| $wajub->refunds | create, retrieve, list |
| $wajub->transfers | create, retrieve, list |
| $wajub->beneficiaries | create, retrieve, update, delete, list |
| $wajub->links | create, retrieve, update, delete, list |
| $wajub->invoices | create, retrieve, update, delete, list, send, markPaid, cancel |
| $wajub->accounts | create, retrieve, update, delete, list, regenerateToken |
| $wajub->webhookEndpoints | create, retrieve, update, delete, list, rotateSecret |
| $wajub->balance | retrieve |
| $wajub->events | list, retrieve, resend |
| $wajub->disputes | list, retrieve, submitEvidence, accept, close, sendMessage |
| $wajub->identity | resolve, validate |
| $wajub->tax | getSettings, updateSettings, rates, calculate, reports, listCodes, retrieveCode, listRegistrations, createRegistration, retrieveRegistration, updateRegistration, deleteRegistration, jurisdictions, thresholds, thresholdAlerts |
| $wajub->shield | getSettings, updateSettings, stats, listBlocklist, addToBlocklist, removeFromBlocklist |
| $wajub->webhooks | constructEvent |
webhookssignature verification runs locally, no HTTP call. All other resources call the merchant REST API.links,invoices,taxandshieldare live only. A sandbox key gets403 This feature is only available in live mode.on every one of their methods.refundsandtransfersare create, retrieve and list only.
Paging through a list
list() returns a PagedResult, which implements Iterator, so foreach over the object walks
the current page and autoPagingIterator() walks all of them.
$page = $wajub->payments->list(['status' => 'succeeded', 'per_page' => 50]);
foreach ($page->data as $payment) {
echo $payment['id'].' '.$payment['amount'];
}
if ($page->hasMore) {
$page = $page->getNextPage();
}
// Or let it fetch the following pages for you.
foreach ($wajub->payments->list()->autoPagingIterator() as $payment) {
$this->reconcile($payment);
}$page->data holds plain arrays, not ApiObject, so rows use array access. $page->meta carries
the list's pagination fields, described on Pagination.
Idempotency and retries
Temporary failures are retried for you; max_network_retries changes how many times. To make a
create safe to repeat on your side too, after a restart or in a queued job's retry, pass your own
idempotency key, the order number for instance.
use Wajub\RequestOptions;
$wajub->payments->create(
$params,
new RequestOptions(idempotencyKey: "order-{$order->id}"),
);Acting for a connected account
use Wajub\RequestOptions;
$wajub->payments->create(
['amount' => 25000, 'currency' => 'XAF', 'email' => $buyer->email],
new RequestOptions(sync: $seller->wajub_account_id),
);Setup is on Sync.
Webhooks
constructEvent verifies the signature and returns the parsed event. It needs the body exactly as
it arrived.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Wajub\Exception\WebhookSignatureVerificationError;
use Wajub\Wajub;
class WajubWebhookController extends Controller
{
public function __invoke(Request $request, Wajub $wajub)
{
try {
$event = $wajub->webhooks->constructEvent(
$request->getContent(),
$request->header('X-Wajub-Signature', ''),
$request->header('X-Wajub-Timestamp', ''),
);
} catch (WebhookSignatureVerificationError) {
return response()->noContent(400);
}
if ($event['event'] === 'payment.succeeded') {
HandleWajubEvent::dispatch($event);
}
return response()->noContent(200);
}
}Use getContent, never all or json
The signature is computed over the raw body. $request->all() and $request->json() parse it,
and a re-encoded body no longer matches. $request->getContent() returns the bytes as delivered.
Two more things a Laravel webhook route needs. Exempt it from CSRF, since Wajub has no session
token, and answer fast. Acknowledge with 200 and let a queued job do the work, because the
delivery is retried when your endpoint takes too long.
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['webhooks/wajub']);
})The event name is in event, not in type. If your clocks drift, constructEvent takes the
tolerance in seconds as a fourth argument. Details on
Signature verification.
Errors
use Wajub\Exception\InvalidRequestException;
use Wajub\Exception\RateLimitException;
use Wajub\Exception\WajubError;
try {
$wajub->payments->create($params);
} catch (InvalidRequestException $e) {
// $e->errors is ['amount' => 'The amount must be at least 25.']
return response()->json(['fields' => $e->errors], 422);
} catch (RateLimitException $e) {
return response()->noContent(503)
->header('Retry-After', (string) ($e->retryAfter ?? 5));
} catch (WajubError $e) {
Log::error('wajub failed', ['code' => $e->errorCode, 'status' => $e->httpStatus]);
throw $e;
}Class in Wajub\Exception | Raised on |
|---|---|
AuthenticationException | 401 |
PermissionException | 403 |
NotFoundException | 404 |
InvalidRequestException | 400 and 422 |
RateLimitException | 429, with retryAfter in seconds |
WajubError | Every other status, and the parent of all of the above |
ApiConnectionException | No response at all, network or timeout |
WebhookSignatureVerificationError | A webhook that did not verify |
getCode always returns zero
Read the error code from $e->errorCode, beside $e->httpStatus, $e->errors and $e->raw:
$e->getCode() is 0 on every Wajub failure.
Testing without touching the API
The http_client option takes any Guzzle client, so a mock handler makes the whole SDK offline.
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Wajub\Wajub;
$mock = new MockHandler([
new Response(201, [], json_encode([
'code' => 201,
'authorization_url' => 'https://pay.wajub.com/test_xxxxx',
'authorization_token' => 'test_xxxxx',
'transaction' => ['id' => 'trx_test', 'status' => 'pending', 'amount' => 25000],
])),
]);
$wajub = new Wajub([
'api_key' => 'sk_test.fake',
'http_client' => new Client(['handler' => HandlerStack::create($mock)]),
]);
$payment = $wajub->payments->create(['amount' => 25000, 'currency' => 'XAF']);
$this->assertSame('test_xxxxx', $payment->authorization_token);Charging without the hosted page
$payment = $wajub->payments->create([
'amount' => 25000,
'currency' => 'XAF',
'phone' => '+237670000000',
]);
$wajub->payments->process($payment->id, [
'channel' => 'cm.mtn',
'phone' => '+237670000000',
]);A channel is country.operator, so cm.mtn is MTN Mobile Money in Cameroon. The full list is on
Mobile Money. The payer still approves on their handset, so the
outcome arrives on the webhook.
Paying out
use Wajub\RequestOptions;
$transfer = $wajub->transfers->create(
[
'amount' => 100000,
'currency' => 'XAF',
'beneficiary' => [
'name' => 'Amina Diallo',
'channel' => 'cm.mtn',
'phone' => '+237670000000',
],
'reference' => 'payout-892',
],
new RequestOptions(idempotencyKey: 'payout-892'),
);beneficiary also takes the ben_… id of a saved beneficiary, which is the better shape once you
pay the same person twice.
Local development
wajub listen --forward-to localhost:8000/webhooks/wajub
wajub trigger payment.succeededMore on the CLI.
Related pages
- SDK QuickstartThe same first call, in five languages side by side.
- WebhooksEvery event, and the delivery guarantees behind them.
- Wajub.jsThe browser half, mounted with the token this SDK returns.
- IdempotencyWhat a key protects and for how long.
- Error handlingWhich failures to retry and which to surface.
- API referenceThe endpoints behind every method above.