Documentation LabPayAPI V2 · V1

SDK et outils

SDK Python

Installer et utiliser labpay : client Python 3.9+ synchrone et typé (httpx), encaissements, envois, remboursements et webhooks avec webhooks.construct_event.

SDK officiel de la V2 pour Python 3.9+, basé sur httpx, entièrement typé (py.typed, mypy --strict). Client synchrone pour l'instant (un client asyncio est prévu).

Version 0.1.0 (aperçu). Code source, exemples, journal des modifications et README complet : dossier python sur GitHub.

Installation#

HTTP
pip install labpay
Bientôt publié

Ce SDK n'est pas encore publié sur PyPI : la commande ci-dessus fonctionnera dès sa publication. En attendant, installez-le depuis le dépôt GitHub des SDK, comme indiqué ci-dessous.

Depuis le dépôt : installez le dossier python par son chemin (dans votre environnement virtuel).

HTTP
git clone https://github.com/Labyrinthe-Team/labyrinthe-pay-packages.git
pip install ./labyrinthe-pay-packages/python

Configuration#

Python
from labpay import LabPay

# Depuis l'environnement : LABPAY_API_KEY (et LABPAY_BASE_URL)
labpay = LabPay()

# Ou explicitement ; le client se ferme avec "with" (ou labpay.close())
with LabPay(
    api_key="test_xxxxxxxxxxxxxxxx",
    base_url="https://api.labyrinthe-rdc.com/api",  # défaut ; "/V2" est ajouté
    timeout=30.0,
    max_retries=2,
    max_retry_wait=10.0,
    auto_reference=True,
) as labpay:
    print(labpay.account.get().live_enabled)
OptionDéfautRôle
api_keyLABPAY_API_KEYToken réel ou clé de test test_….
base_urlLABPAY_BASE_URLRacine de l'API ; /V2 est ajouté.
timeout30.0Secondes, par tentative.
max_retries / max_retry_wait2 / 10.0Relances sûres et attente Retry-After maximale.
auto_referenceTrueGénère sdk_<uuid> quand reference est absente.
http_client—Votre httpx.Client (proxy, tests), non fermé par le SDK.

Montants en str, int ou Decimal (les float sont refusés) ; dans les réponses, des chaînes. Propriétés pratiques : payment.is_completed, is_failed, is_final, is_queued.

Premier encaissement (mode test)#

Avec la clé de test et le numéro de test 0813456789 (réussite), aucun argent réel ne circule. Le paiement reste pending jusqu'à la validation du client ; ne livrez qu'au statut completed.

POST /V2/deposits
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (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>
    callback_url="https://boutique.example/webhooks/labpay",
    customer_message="Commande 42",
    metadata={"orderId": "42"},
)

print(payment.id, payment.status)  # DEP-… pending

Sans reference, le SDK en génère une (sdk_<uuid>) : un renvoi après une coupure réseau rend le même paiement.

Référence automatique
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (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")
print(payment.reference)  # sdk_…

# Désactiver : LabPay(api_key="test_xxxxxxxxxxxxxxxx", auto_reference=False)

Suivre et synchroniser#

Le statut final arrive par notification. En filet de sécurité, relisez le paiement, ou forcez la relecture auprès de LabPay avec sync (sans boucle serrée).

GET /V2/payments/{id}
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

payment = labpay.payments.get("DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6")
if payment.is_completed:
    ...  # livrer la commande payment.reference
POST /V2/payments/{id}/sync
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

payment = labpay.payments.sync("DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6")  # relit le statut tout de suite
GET /V2/payments (liste, toutes les pages, par référence)
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

page = labpay.payments.list(status="completed", operation="deposit", per_page=50)
for payment in page.data:
    print(payment.id, payment.amount)

# Toutes les pages, à la demande :
for payment in labpay.payments.iterate(status="completed"):
    ...

# Par votre référence (None si inconnue) :
found = labpay.payments.find_by_reference("ORDER-42")

Envoyer de l'argent#

Votre solde est débité de amount + frais à l'initiation, et recrédité si l'envoi échoue. Un envoi mis en file (ENQUEUED) peut être annulé.

POST /V2/payouts
from labpay import InsufficientBalanceError, LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

try:
    payout = labpay.payouts.create(
        amount="5000",
        currency="CDF",
        phone="0813456789",
        reference="PAYOUT-17",
        callback_url="https://boutique.example/webhooks/labpay",
        metadata={"payoutId": "17"},
    )
    print(payout.id, payout.total_amount)  # montant + frais débités
except InsufficientBalanceError:
    pass  # rien n'a été envoyé
POST /V2/payouts/{id}/cancel
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

payout = labpay.payments.get("PAY-01JCX9A1B2C3D4E5F6G7H8J9K0")
if payout.is_queued:  # envoi ENQUEUED : annulable
    payout = labpay.payouts.cancel(payout.id)

Rembourser#

Tout ou partie d'un dépôt réussi, vers le wallet qui a payé ; un seul remboursement en cours à la fois par dépôt.

POST /V2/refunds
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

refund = labpay.refunds.create(
    payment_id="DEP-01JCX8Q3T6Y0V5W1Z2A3B4C5D6",
    amount="300",  # facultatif : par défaut, tout le reste remboursable
    reference="REFUND-42-1",
)

Opérateurs, prédiction et soldes#

POST /V2/predict-provider
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

prediction = labpay.providers.predict("0813456789", country="COD")
print(prediction.provider)  # VODACOM_MPESA_COD
GET /V2/providers
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

providers = labpay.providers.list(operation="payout", country="COD")
GET /V2/balances
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

balances = labpay.account.balances()
cdf = labpay.account.balance("CDF")  # None si aucun solde dans cette devise
print(cdf.balance if cdf else "0.00")

Aussi : pays ouverts (GET /V2/countries), disponibilité (GET /V2/availability), compte (GET /V2/account) et rotations du secret de signature et de la clé de test (token réel). Exemples sur Opérateurs et prédiction et Compte et soldes.

Gestion des erreurs#

Toutes les erreurs étendent LabPayError ; celles de l'API étendent ApiError (code : error.code, statut HTTP, erreurs par champ, Retry-After, mode). Un paiement refusé dès l'initiation n'est pas une erreur : il est renvoyé avec le statut failed. Liste complète des codes : Erreurs.

Python
from labpay import APIConnectionError, ApiError, LabPay, ServiceUnavailableError, ValidationError

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

try:
    payment = labpay.deposits.create(amount="1000", currency="CDF", phone="0813456789", reference="ORDER-42")
    if payment.is_failed:
        # refusé dès l'initiation (422 avec le paiement) : renvoyé, pas levé
        print(payment.failure.code if payment.failure else None)
except ValidationError as error:
    print(error.errors)  # messages par champ
except ServiceUnavailableError as error:
    print(error.code, error.retry_after)  # MAINTENANCE, DEPOSITS_DISABLED…
except APIConnectionError:
    pass  # PAS un échec : renvoyer la même référence
except ApiError as error:
    print(error.code)  # toujours le code, jamais le message
Code ou casClasse
UNAUTHENTICATED (401)AuthenticationError
TOKEN_BLOCKED, MERCHANT_INACTIVE, KYC_REQUIRED, LIVE_TOKEN_REQUIRED (403)PermissionDeniedError
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_NUMBERInvalidPhoneNumberError
AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE, AMOUNT_OUT_OF_BOUNDSAmountOutOfRangeError
INSUFFICIENT_BALANCEInsufficientBalanceError
INVALID_CURRENCY, UNSUPPORTED_COUNTRY, UNSUPPORTED_PROVIDER, INVALID_AMOUNTInvalidRequestError
429 (limite de débit) / DAILY_LIMIT_REACHEDRateLimitError / DailyLimitReachedError
MAINTENANCE, API_DISABLED, DEPOSITS_DISABLED, WITHDRAWALS_DISABLED, PROVIDER_TEMPORARILY_UNAVAILABLE, GATEWAY_UNAVAILABLE, V2_DISABLED, TEST_MODE_DISABLED, autres 5xxServiceUnavailableError
Erreur réseau ou délai dépasséAPIConnectionError / APITimeoutError
Saisie invalide, détectée avant tout appelInvalidArgumentError
Notification à la signature ou au corps invalideSignatureVerificationError / InvalidWebhookPayloadError

Les codes API_VERSION_DISABLED et FEATURE_DISABLED (version ou opération coupée depuis la console, 503) sont plus récents que le SDK 0.1.0 : ils arrivent comme ApiError de la classe du statut 503 (service indisponible), avec leur code intact. Ne relancez pas en boucle : voir Fonctionnalités suspendues.

Recevoir les notifications (webhooks)#

webhooks.construct_event(raw_body, header, secret) (module labpay.webhooks) vérifie la signature sur le corps brut (request.get_data() avec Flask, await request.body() avec FastAPI), puis renvoie l'événement typé (is_payment_completed, payment). Une liste de secrets est acceptée pendant une rotation ; webhooks.sign() fabrique un en-tête pour vos tests.

Point de réception (callback_url)
import os

from flask import Flask, abort, request
from labpay import InvalidWebhookPayloadError, SignatureVerificationError, webhooks

app = Flask(__name__)
SECRET = os.environ["LABPAY_WEBHOOK_SECRET"]  # whsec_xxxxxxxxxxxxxxxx


@app.post("/webhooks/labpay")
def labpay_webhook():
    try:
        event = webhooks.construct_event(
            request.get_data(),  # corps BRUT
            request.headers.get(webhooks.SIGNATURE_HEADER),
            SECRET,
        )
    except (SignatureVerificationError, InvalidWebhookPayloadError):
        abort(400)
    if event.is_payment_completed:
        ...  # livrer event.payment.reference, une seule fois (X-Labyrinthe-Delivery ou payment.id)
    return "", 200  # en moins de 10 secondes
  • Le secret whsec_… se lit sur GET /V2/account avec le token réel (variable LABPAY_WEBHOOK_SECRET).
  • Signature ou corps invalide : répondez 400. Sinon 200 en moins de 10 secondes, traitements longs après.
  • Une même notification peut arriver plusieurs fois : dédoublonnez sur X-Labyrinthe-Delivery ou l'id du paiement.

Historique et renvoi#

GET /V2/notifications, POST /V2/notifications/{id}/resend
from labpay import LabPay

labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx")  # ou LabPay() (LABPAY_API_KEY)

page = labpay.notifications.list(status="abandoned")
for notification in page.data:
    labpay.notifications.resend(notification.id)  # nouvelle notification, historique conservé

Comportements communs#

  • Clé `test_…` = sandbox : aucun argent réel, soldes et paiements séparés. Passer en production = changer de clé.
  • Référence automatique : sans reference, le SDK envoie sdk_<uuid>, pour pouvoir relancer un dépôt, un envoi ou un remboursement après une coupure réseau ou un 5xx sans double paiement (idempotence). Désactivable (autoReference / auto_reference / AutoReference).
  • Un paiement refusé est un paiement : un 422 qui porte le paiement (refus dès l'initiation) est renvoyé avec status: failed et failure, pas levé en exception ; un 202 est un paiement pending.
  • Erreurs typées par `code` (puis par statut HTTP), avec les erreurs de validation par champ ; un code inconnu reste lisible. Basez votre logique sur le code, jamais sur le message (en français).
  • Relances sûres (2 par défaut) : lectures et paiements avec référence, 429 après Retry-After, refus de type maintenance (503) seulement après un Retry-After ; jamais DAILY_LIMIT_REACHED. Rotations, renvoi de notification, annulation d'envoi et paiement sans référence ne sont jamais relancés après une erreur réseau ou un 5xx.
  • Validation locale avant tout envoi (montant, devise, numéro, référence, URL…) : une saisie invalide lève une erreur d'argument sans appel réseau.
  • Webhooks vérifiés : constructEvent contrôle X-Labyrinthe-Signature sur le corps brut, en temps constant, avec une tolérance de 300 s et les deux secrets pendant une rotation.
  • Secret par défaut : la clé n'apparaît jamais dans les journaux, les dumps ni les messages d'exception.

Voir aussi : tous les SDK et outils, référence des endpoints V2, codes d'erreur, Postman et OpenAPI.