Aller au contenu

Python

Le package wajub, ses ressources et le problème qui bloque actuellement pip.

Le SDK Python de Wajub est un client synchrone léger pour l'API marchande. Il conserve votre clé secrète, crée des paiements, lit leur statut réel et vérifie les signatures des webhooks. Il ne possède qu'une dépendance : httpx.

Version
1.1.1
Runtime
Python 3.10+, httpx 0.27+

Couvre

  • Paiements
  • Facturation
  • Transferts
  • Sync
  • Shield
  • Taxes

En attendant une version corrigée, installez le package depuis le tag. Sa construction depuis le code source fonctionne correctement.

Installer depuis le dépôt
pip install "git+https://github.com/wajubhq/wajub-python@v1.1.1"

Une fois la version corrigée, la commande habituelle fonctionnera sans autre changement sur cette page.

pip install wajub

Créer le client

Le constructeur accepte uniquement des arguments nommés et utilise les variables d'environnement en repli. Dans le cas courant, aucun argument n'est donc nécessaire.

wajub_client.py
import os

from wajub import Wajub

# Reads WAJUB_API_KEY and WAJUB_WEBHOOK_SECRET from the environment.
wajub = Wajub()

# Or pass them yourself.
wajub = Wajub(
    api_key=os.environ["WAJUB_API_KEY"],
    webhook_secret=os.environ["WAJUB_WEBHOOK_SECRET"],
)
Argument nomméRôle
api_keyVotre clé sk. ou sk_test.. Utilise WAJUB_API_KEY en repli
webhook_secretLe secret whsec_. Utilise WAJUB_WEBHOOK_SECRET en repli
idempotency_key_prefixPréfixe de la clé générée. Valeur par défaut : "wajub"
http_clientVotre propre httpx.Client, si vous avez besoin d'un proxy ou d'un transport monté
max_network_retriesNouvelles tentatives après les erreurs temporaires. Valeur par défaut : 2

Une clé vide déclenche ValueError: Wajub: api_key is required lors de la construction, pas au premier appel. Une mauvaise configuration échoue donc dès l'import au lieu d'échouer en production.

Un gestionnaire de contexte

Wajub possède un httpx.Client et son pool de connexions. Vous devez le fermer dans un script, mais ce n'est pas nécessaire dans un processus web de longue durée.

Un script ferme le client, un serveur le conserve
# A script, a task, a notebook: let the block close the pool.
with Wajub() as wajub:
    payment = wajub.payments.create({"amount": 25000, "currency": "XAF"})

# A Django or FastAPI process: build it once at import and leave it open.
wajub = Wajub()

Fournissez votre propre client au lieu d'utiliser async

Le SDK est synchrone. Dans une vue async def, enveloppez un appel avec asyncio.to_thread ou exécutez-le dans le pool de threads de votre framework. Passer un httpx.AsyncClient à http_client= ne fonctionne pas, car les appels utilisent simplement client.request.

Le premier appel

Créer un paiement
payment = wajub.payments.create(
    {
        "amount": 25000,
        "currency": "XAF",
        "email": "amina@example.com",
        "description": "Order 4172",
        "reference": "order-4172",
        "callback": "https://shop.example.com/complete",
    }
)

print(payment.id)
print(payment.authorization_url)

Les paramètres sont transmis dans un seul dictionnaire, pas comme arguments nommés, et conservent les noms de champs exacts de l'API. Le résultat est un ApiObject. Il expose chaque champ comme un attribut tout en se comportant comme un mapping.

Deux façons de lire le même champ
payment.authorization_url
payment["authorization_url"]

# A field the API added after this release is still reachable.
payment["settlement_batch_id"]

Les montants utilisent l'unité principale

25000 avec XAF représente vingt-cinq mille francs. Une devise décimale accepte un nombre décimal : "amount": 12.50 avec GHS.

Toutes les ressources du client

AttributeMéthodes
wajub.global_ping, channels, countries, currencies
wajub.paymentscreate, initialize, retrieve, list, cancel, process, process_split, list_refunds
wajub.customerscreate, retrieve, update, delete, list, block, unblock, activate, deactivate, list_tax_ids, create_tax_id, delete_tax_id
wajub.refundscreate, retrieve, list
wajub.transferscreate, retrieve, list
wajub.beneficiariescreate, retrieve, update, delete, list
wajub.linkscreate, retrieve, update, delete, list
wajub.invoicescreate, retrieve, update, delete, list, send, mark_paid, cancel
wajub.accountscreate, retrieve, update, delete, list, regenerate_token
wajub.webhook_endpointscreate, retrieve, update, delete, list, rotate_secret
wajub.balanceretrieve
wajub.eventslist, retrieve, resend
wajub.disputeslist, retrieve, submit_evidence, accept, close, send_message
wajub.identityresolve, validate
wajub.taxget_settings, update_settings, rates, calculate, reports, list_codes, retrieve_code, list_registrations, create_registration, retrieve_registration, update_registration, delete_registration, jurisdictions, thresholds, threshold_alerts
wajub.shieldget_settings, update_settings, stats, list_blocklist, add_to_blocklist, remove_from_blocklist
wajub.listenconfig, auth
wajub.webhooksconstruct_event
  • global is a Python keyword, so the accessor is wajub.global_ for ping, channels, countries and currencies.
  • webhooks signature verification runs locally, no HTTP call. All other resources call the merchant REST API.
  • links, invoices, tax and shield are live only. A sandbox key gets 403 This feature is only available in live mode. on every one of their methods.
  • refunds and transfers are create, retrieve and list only. The shared CRUD base also exposes update and delete on them, but the API serves no such route.

Parcourir une liste par pages

list() renvoie un PagedResult. Il contient les lignes, les métadonnées et deux méthodes pour continuer.

Une page ou toutes les pages
page = wajub.payments.list({"status": "success", "per_page": 50})

# page.data holds plain dicts, not ApiObject, so use item access here.
for payment in page.data:
    print(payment["id"], payment["amount"])

if page.has_more:
    page = page.get_next_page()

# Or let it walk every page for you.
for payment in wajub.payments.list().auto_paging_iter():
    reconcile(payment)

Idempotence et nouvelles tentatives

Chaque requête POST et PUT contient une Idempotency-Key générée. Cette clé sécurise les nouvelles tentatives automatiques. Fournissez la vôtre dès que vous disposez d'un identifiant naturel.

Un numéro de commande constitue la meilleure clé
from wajub import RequestOptions

wajub.payments.create(
    params,
    RequestOptions(idempotency_key=f"order-{order_id}"),
)
ÉlémentValeur
Statuts concernés429, 500, 502, 503, 504 et toute erreur réseau
Tentatives3 au total, l'appel initial et deux nouvelles tentatives
Attente progressiveExponentielle avec une part d'aléa (jitter), en respectant Retry-After pour une erreur 429
Délai d'expiration30 secondes par requête, configuré sur le httpx.Client par défaut

Pour modifier le délai d'expiration, fournissez votre propre client : Wajub(http_client=httpx.Client(timeout=60.0)).

Agir pour un compte connecté

Un appel pour un vendeur
from wajub import RequestOptions

wajub.payments.create(
    {"amount": 25000, "currency": "XAF", "email": buyer.email},
    RequestOptions(sync=seller.wajub_account_id),
)

sync devient l'en-tête X-Sync. La configuration est présentée dans Sync.

Webhooks

construct_event vérifie la signature et renvoie l'événement analysé. Il a besoin du corps exactement tel qu'il est arrivé.

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

from .wajub_client import wajub

@csrf_exempt
def wajub_webhook(request):
    try:
        event = wajub.webhooks.construct_event(
            request.body,
            request.headers["X-Wajub-Signature"],
            request.headers["X-Wajub-Timestamp"],
        )
    except Exception:
        return HttpResponse(status=400)

    if event["event"] == "payment.succeeded":
        fulfil(event["data"])

    return HttpResponse(status=200)

La tolérance est de 300 secondes par défaut. construct_event accepte un quatrième argument si le décalage de vos horloges est plus important. Consultez Vérification de signature.

Erreurs

Intercepter d'abord l'erreur précise
from wajub import InvalidRequestError, RateLimitError, WajubError

try:
    wajub.payments.create(params)
except InvalidRequestError as error:
    # error.errors is {"amount": ["The amount must be at least 25."]}
    return JsonResponse({"fields": error.errors}, status=422)
except RateLimitError as error:
    return HttpResponse(status=503, headers={"Retry-After": str(error.retry_after or 5)})
except WajubError as error:
    logger.error("wajub failed", extra={"code": error.code, "status": error.http_status})
    raise
ClasseCas de déclenchement
AuthenticationError401
PermissionError403
NotFoundError404
InvalidRequestError400 et 422
RateLimitError429, avec retry_after en secondes
WajubErrorTout autre statut. C'est aussi la classe de base de toutes les erreurs ci-dessus
ApiConnectionErrorAucune réponse, à cause du réseau ou d'un délai d'expiration
WebhookSignatureVerificationErrorUn webhook dont la signature n'a pas été vérifiée

Toutes contiennent message, code, http_status, errors et raw.

Typage

Le package contient py.typed. mypy et Pyright lisent donc ses annotations sans installation supplémentaire. Les types reflètent les connaissances du SDK : les paramètres sont des dict[str, Any] et les résultats des ApiObject, un mapping dynamique. L'appel est sûr du point de vue des types, mais pas chaque champ de la réponse.

Affinez vous-même le type lorsque nécessaire
from typing import TypedDict

class Checkout(TypedDict):
    session_token: str
    pay_url: str

def start_checkout(order_id: str, amount: float, email: str) -> Checkout:
    payment = wajub.payments.create(
        {
            "amount": amount,
            "currency": "XAF",
            "email": email,
            "reference": order_id,
        },
        RequestOptions(idempotency_key=f"order-{order_id}"),
    )

    return {
        "session_token": payment.authorization_token,
        "pay_url": payment.authorization_url,
    }

Débiter sans page hébergée

Requête push Mobile Money
payment = wajub.payments.create(
    {"amount": 25000, "currency": "XAF", "phone": "+237670000000"}
)

wajub.payments.process(
    payment.id,
    {"channel": "cm.mtn", "phone": "+237670000000"},
)

Un canal suit le format country.operator. Ainsi, cm.mtn désigne MTN Mobile Money au Cameroun. La liste complète se trouve dans Moyens de paiement et canaux. Le payeur doit toujours approuver l'opération sur son téléphone. Le résultat arrive donc par webhook.

Effectuer un payout

Un transfert vers un numéro de téléphone
transfer = wajub.transfers.create(
    {
        "amount": 100000,
        "currency": "XAF",
        "beneficiary": {
            "name": "Amina Diallo",
            "channel": "cm.mtn",
            "phone": "+237670000000",
        },
        "reference": "payout-892",
    },
    RequestOptions(idempotency_key="payout-892"),
)

beneficiary accepte aussi l'identifiant ben_… d'un bénéficiaire enregistré. Cette forme est préférable dès que vous payez plusieurs fois la même personne.

Développement local

Transférez les webhooks vers votre machine avec la CLI au lieu d'exposer un tunnel.

Deux terminaux
wajub listen --forward-to localhost:8000/webhooks/wajub
wajub trigger payment.succeeded

Pour en savoir plus, consultez la CLI.

Que pensez-vous de ce contenu ?