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#
composer require labyrinthe/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: 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).
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.0Configuration#
<?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)| Option | Default | Role |
|---|---|---|
apiKey | LABPAY_API_KEY | Live token or test_… test key (sandbox). Server-side only. |
baseUrl | https://api.labyrinthe-rdc.com/api | API root (LABPAY_BASE_URL); /V2 is added. |
timeout | 30.0 | Seconds, per attempt. |
maxRetries / maxRetryWait | 2 / 10.0 | Safe retries and longest accepted Retry-After wait. |
autoReference | true | Generates 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.
<?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-… pendingWithout 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
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
}<?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<?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.
<?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é
}<?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.
<?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#
<?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<?php
use Labyrinthe\LabPay\Client;
$labpay = new Client('test_xxxxxxxxxxxxxxxx'); // ou Client::fromEnv() (LABPAY_API_KEY)
$providers = $labpay->providers->list(operation: 'payout', country: 'COD');<?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 deviseAlso: 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
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 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)#
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.
<?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 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
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 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.