Aller au contenu

Python

Le package wajub, ses ressources et la version à installer.

Le package wajub est le client Wajub de votre serveur : avec votre clé secrète, il crée des paiements, lit leur statut et vérifie les signatures des webhooks. Ses appels sont synchrones.

Version
1.2.1
Runtime
Python 3.10+, httpx 0.27+

Couvre

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

Installer

pip install "wajub>=1.2.1"

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
http_clientVotre propre httpx.Client, pour un proxy ou un transport monté. Donnez-lui base_url="https://api.wajub.com"
max_network_retriesNombre de nouvelles tentatives après un échec temporaire

Un gestionnaire de contexte

Dans un script, une tâche ou un notebook, ouvrez-le dans un bloc with pour qu'il soit fermé à la fin du bloc. Dans un processus web, créez-le une fois et laissez-le ouvert.

Un script ferme le client, un serveur le conserve
# A script, a task, a notebook: let the block close it.
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()

Dans du code async, utilisez un thread

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. http_client= accepte un httpx.Client, pas un httpx.AsyncClient.

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"]

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

AttributMéthodes
wajub.global_ping, channels, countries, currencies
wajub.paymentscreate, 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.webhooksconstruct_event
  • global est un mot-clé de Python : l'accesseur de ping, channels, countries et currencies est donc wajub.global_.
  • La vérification de signature de webhooks s'exécute en local, sans appel HTTP. Toutes les autres ressources appellent l'API REST.
  • links, invoices, tax et shield ne fonctionnent qu'en live. Une clé sandbox reçoit 403 This feature is only available in live mode. sur chacune de leurs méthodes.
  • refunds et transfers ne proposent que la création, la lecture et la liste.

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": "succeeded", "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

Les échecs temporaires sont réessayés pour vous ; max_network_retries en change le nombre. Pour qu'une création puisse aussi être répétée sans risque de votre côté, fournissez votre propre clé d'idempotence 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}"),
)

Pour régler le délai d'expiration des requêtes, fournissez votre propre client :

Un délai plus long
import httpx

wajub = Wajub(http_client=httpx.Client(base_url="https://api.wajub.com", 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),
)

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)

Si vos horloges dérivent, construct_event accepte la tolérance en secondes comme quatrième argument. 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 vaut {"amount": "The amount must be at least 25."}, une chaîne par champ
    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

mypy et Pyright lisent les annotations du package sans installation supplémentaire. Les paramètres sont des dict[str, Any] et les résultats des ApiObject : typez vous-même les champs dont vous dépendez.

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 Mobile Money. 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 ?