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#
pip install labpayCe 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).
git clone https://github.com/Labyrinthe-Team/labyrinthe-pay-packages.git
pip install ./labyrinthe-pay-packages/pythonConfiguration#
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)| Option | Défaut | Rôle |
|---|---|---|
api_key | LABPAY_API_KEY | Token réel ou clé de test test_…. |
base_url | LABPAY_BASE_URL | Racine de l'API ; /V2 est ajouté. |
timeout | 30.0 | Secondes, par tentative. |
max_retries / max_retry_wait | 2 / 10.0 | Relances sûres et attente Retry-After maximale. |
auto_reference | True | Gé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.
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-… pendingSans reference, le SDK en génère une (sdk_<uuid>) : un renvoi après une coupure réseau rend le même paiement.
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).
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.referencefrom 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 suitefrom 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é.
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é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.
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#
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_CODfrom labpay import LabPay
labpay = LabPay(api_key="test_xxxxxxxxxxxxxxxx") # ou LabPay() (LABPAY_API_KEY)
providers = labpay.providers.list(operation="payout", country="COD")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.
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 cas | Classe |
|---|---|
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_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 (limite de débit) / DAILY_LIMIT_REACHED | RateLimitError / DailyLimitReachedError |
MAINTENANCE, API_DISABLED, DEPOSITS_DISABLED, WITHDRAWALS_DISABLED, PROVIDER_TEMPORARILY_UNAVAILABLE, GATEWAY_UNAVAILABLE, V2_DISABLED, TEST_MODE_DISABLED, autres 5xx | ServiceUnavailableError |
| Erreur réseau ou délai dépassé | APIConnectionError / APITimeoutError |
| Saisie invalide, détectée avant tout appel | InvalidArgumentError |
| Notification à la signature ou au corps invalide | SignatureVerificationError / 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.
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 surGET /V2/accountavec le token réel (variableLABPAY_WEBHOOK_SECRET). - Signature ou corps invalide : répondez
400. Sinon200en moins de 10 secondes, traitements longs après. - Une même notification peut arriver plusieurs fois : dédoublonnez sur
X-Labyrinthe-Deliveryou l'iddu paiement.
Historique et renvoi#
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 envoiesdk_<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
422qui porte le paiement (refus dès l'initiation) est renvoyé avecstatus: failedetfailure, pas levé en exception ; un202est un paiementpending. - 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,
429aprèsRetry-After, refus de type maintenance (503) seulement après unRetry-After; jamaisDAILY_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 :
constructEventcontrôleX-Labyrinthe-Signaturesur 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.