SDKs and tools
Laravel SDK
Install and use labyrinthe/labpay-laravel: injectable client, LabPay facade, publishable config and a signed webhook route with events.
Laravel 10 to 13 integration (PHP 8.1+), on top of the PHP SDK: client singleton, facade, publishable config, a POST /labpay/webhook route that verifies the signature and dispatches events. The whole PHP SDK API is available.
Version 0.1.0 (preview). Source code, examples, changelog and full README: laravel folder on GitHub.
Installation#
composer require labyrinthe/labpay-laravel
php artisan vendor:publish --tag=labpay-config # facultatif : config/labpay.phpThis SDK is not published on Packagist yet: the command above will work once it is released. Until then, install it from the SDK GitHub repository, as shown below.
From the repository: declare both the php and laravel folders as Composer path repositories.
git clone https://github.com/Labyrinthe-Team/labyrinthe-pay-packages.git
composer config repositories.labpay-php '{"type": "path", "url": "../labyrinthe-pay-packages/php", "options": {"versions": {"labyrinthe/labpay-php": "0.1.0"}}}'
composer config repositories.labpay-laravel '{"type": "path", "url": "../labyrinthe-pay-packages/laravel", "options": {"versions": {"labyrinthe/labpay-laravel": "0.1.0"}}}'
composer require labyrinthe/labpay-laravel:^0.1.0Configuration#
LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx # clé de test (sandbox) ; token réel en production
LABPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx # GET /V2/account avec le token réel ; "nouveau,ancien" pendant une rotation
# LABPAY_BASE_URL=https://api.labyrinthe-rdc.com/api
# LABPAY_TIMEOUT=30
# LABPAY_MAX_RETRIES=2
# LABPAY_WEBHOOK_PATH=labpay/webhook (null pour déclarer votre propre route)| Option | Default | Role |
|---|---|---|
labpay.api_key | LABPAY_API_KEY | Live token or test key. |
labpay.base_url | LABPAY_BASE_URL | API root; /V2 is added. |
labpay.timeout / labpay.max_retries | 30 / 2 | LABPAY_TIMEOUT, LABPAY_MAX_RETRIES. |
labpay.auto_reference | true | sdk_<uuid> reference when absent. |
labpay.webhook.secret | LABPAY_WEBHOOK_SECRET | Signing secret; two comma-separated secrets during a rotation. |
labpay.webhook.path | labpay/webhook | Path of the receiving route (LABPAY_WEBHOOK_PATH), null for your own. |
labpay.webhook.middleware / tolerance | [] / 300 | Route middleware (never web: no CSRF token); maximum signature age, in seconds. |
<?php
use Labyrinthe\LabPay\Client;
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
// Injection du client…
public function pay(Order $order, Client $labpay)
{
$payment = $labpay->deposits->create(
amount: $order->total, // "1000" ou "12.50"
currency: 'CDF',
phone: $order->phone,
reference: 'ORDER-'.$order->id, // un renvoi rend le même paiement
callbackUrl: route('labpay.webhook'),
);
}
// … ou façade : les services sont des propriétés du client
LabPay::client()->payments->get('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6');First collection (test mode)#
With the test key and the test number 0813456789 (success), no real money moves. The payment stays pending until the customer confirms; only deliver on completed.
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$payment = LabPay::client()->deposits->create(
amount: '1000',
currency: 'CDF',
phone: '0813456789',
reference: 'ORDER-42',
callbackUrl: route('labpay.webhook'), // route installée par le package
customerMessage: 'Commande 42',
metadata: ['orderId' => '42'],
);Without reference, the SDK generates one (sdk_<uuid>): a resend after a network failure returns the same payment.
<?php
use Labyrinthe\LabPay\Client;
$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)
// Sans reference : le SDK envoie sdk_<uuid> et peut relancer sans risque de double paiement.
$payment = $labpay->deposits->create(amount: '1000', currency: 'CDF', phone: '0813456789');
echo $payment->reference; // sdk_…
// Désactiver : new Client('test_xxxxxxxxxxxxxxxx', autoReference: false)Track and synchronise#
The final status arrives by notification. As a safety net, read the payment, or force a refresh from LabPay with sync (without a tight loop).
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$payment = LabPay::client()->payments->get('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6');<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$payment = LabPay::client()->payments->sync('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6');<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$page = LabPay::client()->payments->list(status: 'completed', operation: 'deposit', perPage: 50);
$payment = LabPay::client()->payments->findByReference('ORDER-42');Send money#
Your balance is debited with amount + fees at initiation, and credited back if the payout fails. A queued payout (ENQUEUED) can be cancelled.
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$payout = LabPay::client()->payouts->create(
amount: '5000',
currency: 'CDF',
phone: '0813456789',
reference: 'PAYOUT-17',
callbackUrl: route('labpay.webhook'),
metadata: ['payoutId' => '17'],
);<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$payout = LabPay::client()->payouts->cancel('PAY-01JCX9A1B2C3D4E5F6G7H8J9K0');Refund#
All or part of a completed deposit, to the wallet that paid; only one refund in progress at a time per deposit.
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$refund = LabPay::client()->refunds->create(
paymentId: 'DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6',
amount: '300',
reference: 'REFUND-42-1',
);Providers, prediction and balances#
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$prediction = LabPay::client()->providers->predict('0813456789', 'COD');<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$providers = LabPay::client()->providers->list(operation: 'payout', country: 'COD');<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$cdf = LabPay::client()->account->balance('CDF');Also: open countries (GET /V2/countries), availability (GET /V2/availability), account (GET /V2/account) and rotations of the signing secret and test key (live token). Examples on Providers and prediction and Account and balances.
Error handling#
Every error extends LabPayException; API errors extend ApiException (code: $e->getErrorCode(), HTTP status, per-field errors, Retry-After, mode). A payment refused at initiation is not an error: it is returned with the failed status. Full list of codes: Errors.
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Exception\ApiException;
use Labyrinthe\LabPay\Exception\ConnectionException;
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
try {
$payment = LabPay::client()->deposits->create(amount: '1000', currency: 'CDF', phone: '0813456789', reference: 'ORDER-42');
} catch (ConnectionException $e) {
// PAS un échec : renvoyer la même référence
} catch (ApiException $e) {
report($e); // $e->getErrorCode(), $e->getStatusCode()
}| Code or case | Class |
|---|---|
UNAUTHENTICATED (401) | AuthenticationException |
TOKEN_BLOCKED, MERCHANT_INACTIVE, KYC_REQUIRED, LIVE_TOKEN_REQUIRED (403) | PermissionException |
PAYMENT_NOT_FOUND, DEPOSIT_NOT_FOUND, NOTIFICATION_NOT_FOUND (404) | NotFoundException |
NOT_A_PAYOUT, NOT_CANCELLABLE, DEPOSIT_NOT_COMPLETED, REFUND_IN_PROGRESS, DEPOSIT_ALREADY_REFUNDED (409) | ConflictException |
VALIDATION_ERROR (422) | ValidationException |
INVALID_PHONE_NUMBER | InvalidPhoneNumberException |
AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE, AMOUNT_OUT_OF_BOUNDS | AmountOutOfRangeException |
INSUFFICIENT_BALANCE | InsufficientBalanceException |
INVALID_CURRENCY, UNSUPPORTED_COUNTRY, UNSUPPORTED_PROVIDER, INVALID_AMOUNT | InvalidRequestException |
429 (rate limit) / DAILY_LIMIT_REACHED | RateLimitException / DailyLimitReachedException |
MAINTENANCE, API_DISABLED, DEPOSITS_DISABLED, WITHDRAWALS_DISABLED, PROVIDER_TEMPORARILY_UNAVAILABLE, GATEWAY_UNAVAILABLE, V2_DISABLED, TEST_MODE_DISABLED, other 5xx | ServiceUnavailableException |
| Network error or timeout | ConnectionException |
| Invalid input, detected before any call | InvalidArgumentException |
| Notification with an invalid signature or body | SignatureVerificationException / InvalidWebhookPayloadException |
The API_VERSION_DISABLED and FEATURE_DISABLED codes (version or operation switched off from the console, 503) are newer than SDK 0.1.0: they arrive as an ApiException of the 503 (service unavailable) class, with their code intact. Do not retry in a loop: see Suspended features.
Receive notifications (webhooks)#
The package registers POST /labpay/webhook (name labpay.webhook, no CSRF). The route verifies X-Labyrinthe-Signature on the raw body (400 if invalid, 500 if LABPAY_WEBHOOK_SECRET is missing), dispatches WebhookReceived then PaymentCompleted or PaymentFailed, and answers 200. Listeners run before the answer and LabPay waits 10 seconds: queue long work (ShouldQueue).
<?php
// .env : LABPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx
// Le package installe POST /labpay/webhook (route labpay.webhook) : signature vérifiée, puis événements.
use Illuminate\Support\Facades\Event;
use Labyrinthe\LabPay\Laravel\Events\PaymentCompleted;
Event::listen(function (PaymentCompleted $event) {
// $event->payment()->reference, $event->deliveryId (dédoublonner), $event->mode
Order::where('reference', $event->payment()->reference)->first()?->markAsPaid();
});- The
whsec_…secret is read onGET /V2/accountwith the live token (LABPAY_WEBHOOK_SECRETvariable). - Invalid signature or body: answer
400. Otherwise200within 10 seconds, long work afterwards. - The same notification can arrive more than once: deduplicate on
X-Labyrinthe-Deliveryor the paymentid.
History and resend#
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;
$page = LabPay::client()->notifications->list(status: 'abandoned');Your own route: LABPAY_WEBHOOK_PATH=null and the labpay.signature middleware; the verified event is in the request.
<?php
use Illuminate\Http\Request;
use Labyrinthe\LabPay\Laravel\Http\Middleware\VerifyLabPaySignature;
Route::post('payments/labpay', function (Request $request) {
$event = $request->attributes->get(VerifyLabPaySignature::EVENT); // Labyrinthe\LabPay\Webhook\Event
return response()->noContent(200);
})->middleware('labpay.signature');The same notification can arrive more than once: make your listener idempotent ($event->deliveryId, or the payment id).
Shared behaviour#
- `test_…` key = sandbox: no real money, separate balances and payments. Going live = switching keys.
- Automatic reference: without
reference, the SDK sendssdk_<uuid>, so that a deposit, payout or refund can be retried after a network failure or a 5xx without double payment (idempotency). Can be disabled (autoReference/auto_reference/AutoReference). - A refused payment is a payment: a
422carrying the payment (refused at initiation) is returned withstatus: failedandfailure, not thrown; a202is apendingpayment. - Errors typed by `code` (then by HTTP status), with per-field validation errors; an unknown code stays readable. Base your logic on the code, never on the message (in French).
- Safe retries (2 by default): reads and payments with a reference,
429afterRetry-After, maintenance-type refusals (503) only after aRetry-After; neverDAILY_LIMIT_REACHED. Rotations, notification resends, payout cancellations and payments without reference are never retried after a network error or a 5xx. - Local validation before any request (amount, currency, number, reference, URL…): invalid input raises an argument error with no network call.
- Verified webhooks:
constructEventchecksX-Labyrinthe-Signatureon the raw body, in constant time, with a 300 s tolerance and both secrets during a rotation. - Secure by default: the key never shows up in logs, dumps or exception messages.
See also: all SDKs and tools, V2 endpoint reference, error codes, Postman and OpenAPI.