SDKs and tools
Node.js / TypeScript SDK
Install and use @labyrinthe/labpay: dependency-free TypeScript client, native fetch, ESM and CommonJS, webhooks with Webhooks.constructEvent.
Official V2 SDK for Node.js 18+: native fetch, no runtime dependency, ESM and CommonJS builds with type declarations. The core only uses Fetch and Web Crypto: it also runs on Bun, Deno and edge runtimes — server-side only, never in a browser.
Version 0.1.0 (preview). Source code, examples, changelog and full README: node folder on GitHub.
Installation#
npm install @labyrinthe/labpayThis SDK is not published on npm 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: build the package (dist/ is not versioned), then install it by path.
git clone https://github.com/Labyrinthe-Team/labyrinthe-pay-packages.git
cd labyrinthe-pay-packages/node && npm install && npm run build && cd -
npm install ../labyrinthe-pay-packages/nodeConfiguration#
import { LabPay } from '@labyrinthe/labpay';
// Depuis l'environnement : LABPAY_API_KEY (et LABPAY_BASE_URL)
const labpay = new LabPay();
// Ou explicitement
const client = new LabPay({
apiKey: 'test_xxxxxxxxxxxxxxxx',
baseUrl: 'https://api.labyrinthe-rdc.com/api', // défaut ; "/V2" est ajouté
timeoutMs: 30_000,
maxRetries: 2,
maxRetryWaitMs: 10_000,
autoReference: true,
fetch: customFetch, // facultatif (proxy, tests)
});
client.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. |
baseUrl | LABPAY_BASE_URL | API root; /V2 is added. |
timeoutMs | 30000 | Milliseconds, per attempt. |
maxRetries / maxRetryWaitMs | 2 / 10000 | Safe retries and longest Retry-After wait. |
autoReference | true | Generates sdk_<uuid> when reference is absent. |
fetch | global fetch | fetch implementation to use. |
Responses are mapped to camelCase objects (totalAmount, gatewayStatus…); the raw payment stays in payment.raw. Every method accepts { signal } (AbortSignal) as last argument. Helpers: isCompleted(p), isFailed(p), isFinal(p), isQueued(p).
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.
import { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const payment = await 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' },
});
console.log(payment.id, payment.status); // DEP-… pendingWithout reference, the SDK generates one (sdk_<uuid>): a resend after a network failure returns the same payment.
import { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
// Sans reference : le SDK envoie sdk_<uuid> et peut relancer sans risque de double paiement.
const payment = await labpay.deposits.create({ amount: '1000', currency: 'CDF', phone: '0813456789' });
console.log(payment.reference); // sdk_…
// Désactiver : new LabPay({ apiKey: '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).
import { LabPay, isCompleted } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const payment = await labpay.payments.get('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6');
if (isCompleted(payment)) {
// livrer la commande payment.reference
}import { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const payment = await labpay.payments.sync('DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6'); // relit le statut tout de suiteimport { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const page = await labpay.payments.list({ status: 'completed', operation: 'deposit', perPage: 50 });
for (const payment of page.data) console.log(payment.id, payment.amount);
// Toutes les pages, à la demande :
for await (const payment of labpay.payments.iterate({ status: 'completed' })) { /* … */ }
// Par votre référence (null si inconnue) :
const found = await 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.
import { InsufficientBalanceError, LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
try {
const payout = await labpay.payouts.create({
amount: '5000',
currency: 'CDF',
phone: '0813456789',
reference: 'PAYOUT-17',
callbackUrl: 'https://boutique.example/webhooks/labpay',
metadata: { payoutId: '17' },
});
console.log(payout.id, payout.totalAmount); // montant + frais débités
} catch (error) {
if (error instanceof InsufficientBalanceError) {
// rien n'a été envoyé
} else throw error;
}import { LabPay, isQueued } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
let payout = await labpay.payments.get('PAY-01JCX9A1B2C3D4E5F6G7H8J9K0');
if (isQueued(payout)) { // envoi ENQUEUED : annulable
payout = await 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.
import { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const refund = await labpay.refunds.create({
paymentId: 'DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6',
amount: '300', // facultatif : par défaut, tout le reste remboursable
reference: 'REFUND-42-1',
});Providers, prediction and balances#
import { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const prediction = await labpay.providers.predict({ phone: '0813456789', country: 'COD' });
console.log(prediction.provider); // VODACOM_MPESA_CODimport { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const providers = await labpay.providers.list({ operation: 'payout', country: 'COD' });import { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const balances = await labpay.account.balances();
const cdf = await labpay.account.balance('CDF'); // null si aucun solde dans cette devise
console.log(cdf?.balance); // "125000.00"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 LabPayError; API errors extend ApiError (code: error.code, 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.
import { ApiError, ConnectionError, LabPay, ServiceUnavailableError, ValidationError, isFailed } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
try {
const payment = await labpay.deposits.create({ amount: '1000', currency: 'CDF', phone: '0813456789', reference: 'ORDER-42' });
if (isFailed(payment)) {
// refusé dès l'initiation (422 avec le paiement) : renvoyé, pas levé
console.log(payment.failure?.code);
}
} catch (error) {
if (error instanceof ValidationError) console.log(error.errors); // messages par champ
else if (error instanceof ServiceUnavailableError) console.log(error.code, error.retryAfter); // MAINTENANCE…
else if (error instanceof ConnectionError) { /* PAS un échec : renvoyer la même référence */ }
else if (error instanceof ApiError) console.log(error.code); // toujours le code, jamais le message
else throw error;
}| Code or case | Class |
|---|---|
UNAUTHENTICATED (401) | AuthenticationError |
TOKEN_BLOCKED, MERCHANT_INACTIVE, KYC_REQUIRED, LIVE_TOKEN_REQUIRED (403) | PermissionError |
PAYMENT_NOT_FOUND, DEPOSIT_NOT_FOUND, NOTIFICATION_NOT_FOUND (404) | NotFoundError |
NOT_A_PAYOUT, NOT_CANCELLABLE, DEPOSIT_NOT_COMPLETED, REFUND_IN_PROGRESS, DEPOSIT_ALREADY_REFUNDED (409) | ConflictError |
VALIDATION_ERROR (422) | ValidationError |
INVALID_PHONE_NUMBER | InvalidPhoneNumberError |
AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE, AMOUNT_OUT_OF_BOUNDS | AmountOutOfRangeError |
INSUFFICIENT_BALANCE | InsufficientBalanceError |
INVALID_CURRENCY, UNSUPPORTED_COUNTRY, UNSUPPORTED_PROVIDER, INVALID_AMOUNT | InvalidRequestError |
429 (rate limit) / DAILY_LIMIT_REACHED | RateLimitError / DailyLimitReachedError |
MAINTENANCE, API_DISABLED, DEPOSITS_DISABLED, WITHDRAWALS_DISABLED, PROVIDER_TEMPORARILY_UNAVAILABLE, GATEWAY_UNAVAILABLE, V2_DISABLED, TEST_MODE_DISABLED, other 5xx | ServiceUnavailableError |
| Network error or timeout | ConnectionError / AbortedError |
| Invalid input, detected before any call | InvalidArgumentError |
| Notification with an invalid signature or body | SignatureVerificationError / InvalidWebhookPayloadError |
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 ApiError of the 503 (service unavailable) class, with their code intact. Do not retry in a loop: see Suspended features.
Receive notifications (webhooks)#
Webhooks.constructEvent(rawBody, header, secret) verifies the signature on the raw body (string or bytes: with Express, express.raw()), then returns the typed event (event.event, event.payment). An array of secrets is accepted during a rotation; Webhooks.sign(body, secret) builds a header for your tests.
import { WEBHOOK_HEADERS, Webhooks } from '@labyrinthe/labpay';
// Next.js App Router : app/api/webhooks/labpay/route.ts (Express : express.raw())
export async function POST(request: Request) {
const rawBody = await request.text(); // corps BRUT
try {
const event = await Webhooks.constructEvent(
rawBody,
request.headers.get(WEBHOOK_HEADERS.signature),
process.env.LABPAY_WEBHOOK_SECRET!, // whsec_xxxxxxxxxxxxxxxx
);
if (event.event === 'payment.completed') {
// livrer event.payment.reference, une seule fois (X-Labyrinthe-Delivery ou payment.id)
}
} catch {
return new Response(null, { status: 400 }); // signature ou corps invalide
}
return new Response(null, { status: 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#
import { LabPay } from '@labyrinthe/labpay';
const labpay = new LabPay({ apiKey: 'test_xxxxxxxxxxxxxxxx' }); // ou new LabPay() (LABPAY_API_KEY)
const page = await labpay.notifications.list({ status: 'abandoned' });
for (const notification of page.data) {
await 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.