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.
wajub
Stable · GAPyPI
- Version
- 1.1.1
- Runtime
- Python 3.10+, httpx 0.27+
Couvre
- Paiements
- Facturation
- Transferts
- Sync
- Shield
- Taxes
Le package publié est actuellement vide
pip install wajub réussit, puis import wajub déclenche ModuleNotFoundError. Le wheel 1.1.1
sur PyPI contient un seul fichier, wajub/py.typed, et aucun module. Le code source du dépôt est
complet et correct. Toute la documentation ci-dessous est donc exacte. Seule la distribution est défectueuse.
En attendant une version corrigée, installez le package depuis le tag. Sa construction depuis le code source fonctionne correctement.
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 wajubCré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.
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_key | Votre clé sk. ou sk_test.. Utilise WAJUB_API_KEY en repli |
webhook_secret | Le secret whsec_. Utilise WAJUB_WEBHOOK_SECRET en repli |
idempotency_key_prefix | Préfixe de la clé générée. Valeur par défaut : "wajub" |
http_client | Votre propre httpx.Client, si vous avez besoin d'un proxy ou d'un transport monté |
max_network_retries | Nouvelles 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.
# 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
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.
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
| Attribute | Méthodes |
|---|---|
| wajub.global_ | ping, channels, countries, currencies |
| wajub.payments | create, initialize, retrieve, list, cancel, process, process_split, list_refunds |
| wajub.customers | create, retrieve, update, delete, list, block, unblock, activate, deactivate, list_tax_ids, create_tax_id, delete_tax_id |
| wajub.refunds | create, retrieve, list |
| wajub.transfers | create, retrieve, list |
| wajub.beneficiaries | create, retrieve, update, delete, list |
| wajub.links | create, retrieve, update, delete, list |
| wajub.invoices | create, retrieve, update, delete, list, send, mark_paid, cancel |
| wajub.accounts | create, retrieve, update, delete, list, regenerate_token |
| wajub.webhook_endpoints | create, retrieve, update, delete, list, rotate_secret |
| wajub.balance | retrieve |
| wajub.events | list, retrieve, resend |
| wajub.disputes | list, retrieve, submit_evidence, accept, close, send_message |
| wajub.identity | resolve, validate |
| wajub.tax | get_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.shield | get_settings, update_settings, stats, list_blocklist, add_to_blocklist, remove_from_blocklist |
| wajub.listen | config, auth |
| wajub.webhooks | construct_event |
globalis a Python keyword, so the accessor iswajub.global_for ping, channels, countries and currencies.webhookssignature verification runs locally, no HTTP call. All other resources call the merchant REST API.links,invoices,taxandshieldare live only. A sandbox key gets403 This feature is only available in live mode.on every one of their methods.refundsandtransfersare create, retrieve and list only. The shared CRUD base also exposesupdateanddeleteon 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.
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.
from wajub import RequestOptions
wajub.payments.create(
params,
RequestOptions(idempotency_key=f"order-{order_id}"),
)| Élément | Valeur |
|---|---|
| Statuts concernés | 429, 500, 502, 503, 504 et toute erreur réseau |
| Tentatives | 3 au total, l'appel initial et deux nouvelles tentatives |
| Attente progressive | Exponentielle avec une part d'aléa (jitter), en respectant Retry-After pour une erreur 429 |
| Délai d'expiration | 30 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é
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)request.json n'est pas le corps
La signature couvre {timestamp}.{raw body}. Sérialiser de nouveau un dictionnaire analysé
modifie l'ordre des clés et les espaces. Le hash ne correspond alors plus. Utilisez request.body
dans Django, get_data() dans Flask ou await request.body() dans FastAPI. Toutes ces méthodes
renvoient des octets, comme l'exige construct_event.
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
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| Classe | Cas de déclenchement |
|---|---|
AuthenticationError | 401 |
PermissionError | 403 |
NotFoundError | 404 |
InvalidRequestError | 400 et 422 |
RateLimitError | 429, avec retry_after en secondes |
WajubError | Tout autre statut. C'est aussi la classe de base de toutes les erreurs ci-dessus |
ApiConnectionError | Aucune réponse, à cause du réseau ou d'un délai d'expiration |
WebhookSignatureVerificationError | Un webhook dont la signature n'a pas été vérifiée |
Toutes contiennent message, code, http_status, errors et raw.
PermissionError masque un type intégré
wajub.PermissionError et le PermissionError natif de Python portent le même nom. Un simple
from wajub import * remplace silencieusement le type intégré. Importez les noms que vous utilisez,
ou conservez le module : import wajub puis except wajub.PermissionError.
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.
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
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
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.
wajub listen --forward-to localhost:8000/webhooks/wajub
wajub trigger payment.succeededPour en savoir plus, consultez la CLI.
Pages associées
- Démarrage rapide des SDKsLe même premier appel dans cinq langages, présentés côte à côte.
- WebhooksTous les événements et leurs garanties de livraison.
- Conventions de nommage des SDKsPourquoi ce SDK conserve le snake_case contrairement à Go et Java.
- IdempotenceCe que protège une clé et pendant combien de temps.
- Gestion des erreursLes échecs à retenter et ceux à présenter à l'utilisateur.
- Vue d'ensemble de la référence APILes endpoints utilisés par chaque méthode ci-dessus.