SDKs and tools

PHP SDK

Install and use labyrinthe/labpay-php: collect, send, refund, track and receive signed notifications in PHP 8.1+.

Official V2 PHP SDK: PHP 8.1+, any PSR-18 HTTP client (Guzzle by default), PSR-3 logger. Typed models and exceptions, named arguments.

Version 0.1.0 (preview). Source code, examples, changelog and full README: php folder on GitHub.

Installation#

HTTP
composer require labyrinthe/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: clone it next to your project, then declare the php folder as a Composer path repository (this is how the Laravel and WooCommerce packages use it).

HTTP
git clone https://github.com/Labyrinthe-Team/labyrinthe-pay-packages.git
composer config repositories.labpay '{"type": "path", "url": "../labyrinthe-pay-packages/php", "options": {"versions": {"labyrinthe/labpay-php": "0.1.0"}}}'
composer require labyrinthe/labpay-php:^0.1.0

Configuration#

PHP
<?php
use Labyrinthe\LabPay\Client;

// Depuis l'environnement : LABPAY_API_KEY (obligatoire), LABPAY_BASE_URL (facultatif)
$labpay = Client::fromEnv();

// Ou explicitement (arguments nommés)
$labpay = new Client(
    apiKey: 'test_xxxxxxxxxxxxxxxx',
    baseUrl: 'https://api.labyrinthe-rdc.com/api',   // défaut ; "/V2" est ajouté
    timeout: 30.0,
    maxRetries: 2,
    maxRetryWait: 10.0,
    autoReference: true,
    httpClient: $psr18Client,                         // facultatif
    logger: $psr3Logger,                              // ne reçoit jamais la clé
);

$labpay->isTestMode();      // true avec une clé test_…
$labpay->lastResponse();    // statut et en-têtes de la dernière réponse (->mode() = X-Labyrinthe-Mode)
OptionDefaultRole
apiKeyLABPAY_API_KEYLive token or test_… test key (sandbox). Server-side only.
baseUrlhttps://api.labyrinthe-rdc.com/apiAPI root (LABPAY_BASE_URL); /V2 is added.
timeout30.0Seconds, per attempt.
maxRetries / maxRetryWait2 / 10.0Safe retries and longest accepted Retry-After wait.
autoReferencetrueGenerates sdk_<uuid> when reference is absent.
httpClient, logger—PSR-18 client and PSR-3 logger.

Services are client properties: deposits, payouts, refunds, payments, providers, account, notifications. Amounts as a string ('12.50') or an int; in responses, always strings with 2 decimals.

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
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$payment = $labpay->deposits->create(
    amount: '1000',
    currency: 'CDF',
    phone: '0813456789',
    reference: 'ORDER-42',           // facultatif : sinon le SDK génère sdk_<uuid>
    callbackUrl: 'https://boutique.example/webhooks/labpay',
    customerMessage: 'Commande 42',
    metadata: ['orderId' => '42'],
);

echo $payment->id, ' ', $payment->status->value; // DEP-… pending

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
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$payment = $labpay->payments->get('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6');
if ($payment->isCompleted()) {
    // livrer la commande $payment->reference
}
POST /V2/payments/{id}/sync
<?php
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$payment = $labpay->payments->sync('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6'); // relit le statut tout de suite
GET /V2/payments (list, every page, by reference)
<?php
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$page = $labpay->payments->list(status: 'completed', operation: 'deposit', perPage: 50);
foreach ($page->items as $payment) {
    echo $payment->id, ' ', $payment->amount, PHP_EOL;
}

// Toutes les pages, à la demande :
foreach ($labpay->payments->iterate(status: 'completed') as $payment) { /* … */ }

// Par votre référence (null si inconnue) :
$payment = $labpay->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
use Labyrinthe\LabPay\Client;
use Labyrinthe\LabPay\Exception\InsufficientBalanceException;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

try {
    $payout = $labpay->payouts->create(
        amount: '5000',
        currency: 'CDF',
        phone: '0813456789',
        reference: 'PAYOUT-17',
        callbackUrl: 'https://boutique.example/webhooks/labpay',
        metadata: ['payoutId' => '17'],
    );
    echo $payout->id, ' ', $payout->totalAmount; // montant + frais débités
} catch (InsufficientBalanceException $e) {
    // rien n'a été envoyé
}
POST /V2/payouts/{id}/cancel
<?php
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$payout = $labpay->payments->get('PAY-01JCX9A1B2C3D4E5F6G7H8J9K0');
if ($payout->isQueued()) {                       // envoi ENQUEUED : annulable
    $payout = $labpay->payouts->cancel($payout->id);
}

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
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$refund = $labpay->refunds->create(
    paymentId: 'DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6',
    amount: '300',                   // facultatif : par défaut, tout le reste remboursable
    reference: 'REFUND-42-1',
);

Providers, prediction and balances#

POST /V2/predict-provider
<?php
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$prediction = $labpay->providers->predict('0813456789', 'COD');
echo $prediction->provider; // VODACOM_MPESA_COD
GET /V2/providers
<?php
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$providers = $labpay->providers->list(operation: 'payout', country: 'COD');
GET /V2/balances
<?php
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

foreach ($labpay->account->balances() as $balance) {
    echo $balance->currency, ' ', $balance->balance, PHP_EOL; // CDF 125000.00
}
$cdf = $labpay->account->balance('CDF'); // null si aucun solde dans cette devise

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
<?php
use Labyrinthe\LabPay\Client;
use Labyrinthe\LabPay\Exception\ApiException;
use Labyrinthe\LabPay\Exception\ConnectionException;
use Labyrinthe\LabPay\Exception\ServiceUnavailableException;
use Labyrinthe\LabPay\Exception\ValidationException;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

try {
    $payment = $labpay->deposits->create(amount: '1000', currency: 'CDF', phone: '0813456789', reference: 'ORDER-42');
    if ($payment->isFailed()) {
        // refusé dès l'initiation (422 avec le paiement) : renvoyé, pas levé
        echo $payment->failure->code;
    }
} catch (ValidationException $e) {
    $e->getErrors();                 // messages par champ
} catch (ServiceUnavailableException $e) {
    $e->getErrorCode();              // MAINTENANCE, DEPOSITS_DISABLED… ; $e->getRetryAfter()
} catch (ConnectionException $e) {
    // PAS un échec : renvoyer la même référence ou payments->findByReference('ORDER-42')
} catch (ApiException $e) {
    $e->getErrorCode();              // toujours le code, jamais le message
}
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)#

Webhook::constructEvent() verifies the signature on the raw body and returns a typed event (isPaymentCompleted(), isPaymentFailed(), ->payment). Webhook::sign($body, $secret) builds a header to test your endpoint; pass an array of secrets during your own rotation.

Receiving endpoint (callback_url)
<?php
use Labyrinthe\LabPay\Exception\InvalidWebhookPayloadException;
use Labyrinthe\LabPay\Exception\SignatureVerificationException;
use Labyrinthe\LabPay\Webhook\Webhook;

try {
    $event = Webhook::constructEvent(
        file_get_contents('php://input'),               // corps BRUT
        $_SERVER['HTTP_X_LABYRINTHE_SIGNATURE'] ?? '',
        getenv('LABPAY_WEBHOOK_SECRET'),                // whsec_xxxxxxxxxxxxxxxx
    );
} catch (SignatureVerificationException|InvalidWebhookPayloadException) {
    http_response_code(400);
    exit;
}

if ($event->isPaymentCompleted()) {
    // livrer $event->payment->reference, une seule fois (X-Labyrinthe-Delivery ou payment->id)
}
http_response_code(200);                                 // en moins de 10 secondes
  • 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
use Labyrinthe\LabPay\Client;

$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)

$page = $labpay->notifications->list(status: 'abandoned');
foreach ($page->items as $notification) {
    $labpay->notifications->resend($notification->id); // nouvelle notification, historique conservé
}

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.