Python
The wajub package, its resources, and the version to install.
The wajub package is your server's Wajub client: with your secret key, it creates payments,
reads their status and verifies webhook signatures. Its calls are synchronous.
wajub
Stable · GAPyPI
- Version
- 1.2.1
- Runtime
- Python 3.10+, httpx 0.27+
Covers
- Payments
- Billing
- Transfers
- Sync
- Shield
- Tax
Install
pip install "wajub>=1.2.1"Create the client
The constructor is keyword only, and it falls back to the environment, so the common case is no arguments at all.
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"],
)| Keyword | What it does |
|---|---|
api_key | Your sk. or sk_test. key. Falls back to WAJUB_API_KEY |
webhook_secret | The whsec_ secret. Falls back to WAJUB_WEBHOOK_SECRET |
http_client | Your own httpx.Client, for a proxy or a mounted transport. Give it base_url="https://api.wajub.com" |
max_network_retries | How many times a temporary failure is retried |
It is a context manager
In a script, a task or a notebook, open it in a with block so it is closed when the block ends.
In a web process, build it once and leave it open.
# 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()In async code, use a thread
The SDK is synchronous. In an async def view, wrap a call in asyncio.to_thread or run it on
your framework's thread pool. http_client= takes an httpx.Client, not an httpx.AsyncClient.
The first call
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)Parameters go in as a single dict, not as keyword arguments, and they keep the API's exact field
names. The result is an ApiObject, which exposes every field as an attribute and still behaves
like a mapping.
payment.authorization_url
payment["authorization_url"]Amounts are in the major unit
25000 with XAF is twenty-five thousand francs. A decimal currency takes a decimal:
"amount": 12.50 with GHS.
Every resource on the client
| Attribute | Methods |
|---|---|
| wajub.global_ | ping, channels, countries, currencies |
| wajub.payments | create, 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.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.
Paging through a list
list() returns a PagedResult. It holds the rows, the metadata, and two ways to keep going.
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)Idempotency and retries
Temporary failures are retried for you; max_network_retries changes how many times. To make a
create safe to repeat on your side too, supply your own idempotency key whenever you have a natural
one.
from wajub import RequestOptions
wajub.payments.create(
params,
RequestOptions(idempotency_key=f"order-{order_id}"),
)To set the request timeout, pass your own client:
import httpx
wajub = Wajub(http_client=httpx.Client(base_url="https://api.wajub.com", timeout=60.0))Acting for a connected account
from wajub import RequestOptions
wajub.payments.create(
{"amount": 25000, "currency": "XAF", "email": buyer.email},
RequestOptions(sync=seller.wajub_account_id),
)Setup is on Sync.
Webhooks
construct_event verifies the signature and returns the parsed event. It needs the body exactly
as it arrived.
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 is not the body
The signature is computed over the raw body, and a parsed then re-serialised dict no longer
matches it. Use request.body in Django, get_data() in Flask,
await request.body() in FastAPI. Every one of them returns bytes, which is what
construct_event wants.
If your clocks drift, construct_event takes the tolerance in seconds as a fourth argument. Details
on
Signature verification.
Errors
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."}, one string per field
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| Class | Raised on |
|---|---|
AuthenticationError | 401 |
PermissionError | 403 |
NotFoundError | 404 |
InvalidRequestError | 400 and 422 |
RateLimitError | 429, with retry_after in seconds |
WajubError | Every other status, and the base of all of the above |
ApiConnectionError | No response at all, network or timeout |
WebhookSignatureVerificationError | A webhook that did not verify |
Each one carries message, code, http_status, errors and raw.
PermissionError shadows a builtin
wajub.PermissionError and Python's own PermissionError share a name. A bare
from wajub import * silently replaces the builtin. Import the names you use, or keep the
module: import wajub then except wajub.PermissionError.
Typing
mypy and Pyright read the package's annotations with nothing to install. Parameters are
dict[str, Any] and results are ApiObject, so type the fields you rely on yourself.
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,
}Charging without the hosted page
payment = wajub.payments.create(
{"amount": 25000, "currency": "XAF", "phone": "+237670000000"}
)
wajub.payments.process(
payment.id,
{"channel": "cm.mtn", "phone": "+237670000000"},
)A channel is country.operator, so cm.mtn is MTN Mobile Money in Cameroon. The full list is on
Mobile Money. The payer still approves on their handset, so the
outcome arrives on the webhook.
Paying out
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 also takes the ben_… id of a saved beneficiary, which is the better shape once you
pay the same person twice.
Local development
Forward webhooks to your machine with the CLI rather than exposing a tunnel.
wajub listen --forward-to localhost:8000/webhooks/wajub
wajub trigger payment.succeededMore on the CLI.
Related pages
- SDK QuickstartThe same first call, in five languages side by side.
- WebhooksEvery event, and the delivery guarantees behind them.
- Naming conventionsWhich names follow Python, and which keep the API spelling.
- IdempotencyWhat a key protects and for how long.
- Error handlingWhich failures to retry and which to surface.
- API referenceThe endpoints behind every method above.