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#

HTTP
composer require labyrinthe/labpay-laravel
php artisan vendor:publish --tag=labpay-config   # facultatif : config/labpay.php
Coming soon

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

HTTP
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.0

Configuration#

.env
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)
OptionDefaultRole
labpay.api_keyLABPAY_API_KEYLive token or test key.
labpay.base_urlLABPAY_BASE_URLAPI root; /V2 is added.
labpay.timeout / labpay.max_retries30 / 2LABPAY_TIMEOUT, LABPAY_MAX_RETRIES.
labpay.auto_referencetruesdk_<uuid> reference when absent.
labpay.webhook.secretLABPAY_WEBHOOK_SECRETSigning secret; two comma-separated secrets during a rotation.
labpay.webhook.pathlabpay/webhookPath of the receiving route (LABPAY_WEBHOOK_PATH), null for your own.
labpay.webhook.middleware / tolerance[] / 300Route middleware (never web: no CSRF token); maximum signature age, in seconds.
Use the client
<?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.

POST /V2/deposits
<?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.

Automatic reference
<?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).

GET /V2/payments/{id}
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;

$payment = LabPay::client()->payments->get('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6');
POST /V2/payments/{id}/sync
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;

$payment = LabPay::client()->payments->sync('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6');
GET /V2/payments (list, every page, by reference)
<?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.

POST /V2/payouts
<?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'],
);
POST /V2/payouts/{id}/cancel
<?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.

POST /V2/refunds
<?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#

POST /V2/predict-provider
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;

$prediction = LabPay::client()->providers->predict('0813456789', 'COD');
GET /V2/providers
<?php
// .env : LABPAY_API_KEY=test_xxxxxxxxxxxxxxxx
use Labyrinthe\LabPay\Laravel\Facades\LabPay;

$providers = LabPay::client()->providers->list(operation: 'payout', country: 'COD');
GET /V2/balances
<?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.

Laravel
<?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 caseClass
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_NUMBERInvalidPhoneNumberException
AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE, AMOUNT_OUT_OF_BOUNDSAmountOutOfRangeException
INSUFFICIENT_BALANCEInsufficientBalanceException
INVALID_CURRENCY, UNSUPPORTED_COUNTRY, UNSUPPORTED_PROVIDER, INVALID_AMOUNTInvalidRequestException
429 (rate limit) / DAILY_LIMIT_REACHEDRateLimitException / DailyLimitReachedException
MAINTENANCE, API_DISABLED, DEPOSITS_DISABLED, WITHDRAWALS_DISABLED, PROVIDER_TEMPORARILY_UNAVAILABLE, GATEWAY_UNAVAILABLE, V2_DISABLED, TEST_MODE_DISABLED, other 5xxServiceUnavailableException
Network error or timeoutConnectionException
Invalid input, detected before any callInvalidArgumentException
Notification with an invalid signature or bodySignatureVerificationException / 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).

Receiving endpoint (callback_url)
<?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 on GET /V2/account with the live token (LABPAY_WEBHOOK_SECRET variable).
  • Invalid signature or body: answer 400. Otherwise 200 within 10 seconds, long work afterwards.
  • The same notification can arrive more than once: deduplicate on X-Labyrinthe-Delivery or the payment id.

History and resend#

GET /V2/notifications, POST /V2/notifications/{id}/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.

Custom route
<?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 sends sdk_<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 422 carrying the payment (refused at initiation) is returned with status: failed and failure, not thrown; a 202 is a pending payment.
  • 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, 429 after Retry-After, maintenance-type refusals (503) only after a Retry-After; never DAILY_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: constructEvent checks X-Labyrinthe-Signature on 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.