A comprehensive Python SDK for the Wave Business API (Balance, Checkout, Payout, Aggregated Merchants, Webhooks)
Project description
wave-business-api
A comprehensive, production-ready Python SDK for the Wave Business API.
Supports Balance & Reconciliation, Checkout, Payout, Aggregated Merchants, and Webhooks.
Built by Jean Marie Daniel Vianney Guedegbe — Full Stack Python & JavaScript Developer
GitHub: @daniel10027 · LinkedIn: daniel-guedegbe
Table of Contents
- Installation
- Quick Start
- Authentication
- Balance & Reconciliation API
- Checkout API
- Payout API
- Aggregated Merchants API
- Webhooks
- Error Handling
- IP Whitelisting
- Testing
- Contributing
- License
Installation
pip install wave-business-api
Requirements: Python 3.8+, requests>=2.28
Quick Start
from wave_business_api import WaveClient
client = WaveClient(api_key="wave_sn_prod_YhUNb9d...i4bA6")
# Check your wallet balance
balance = client.balance.get()
print(f"Balance: {balance['amount']} {balance['currency']}")
# Create a payment checkout
session = client.checkout.create_session(
amount="5000",
currency="XOF",
success_url="https://myapp.com/success",
error_url="https://myapp.com/error",
)
print("Redirect user to:", session["wave_launch_url"])
# Send money to someone
payout = client.payout.create(
currency="XOF",
receive_amount="10000",
mobile="+221555110219",
name="Fatou Ndiaye",
)
print("Payout status:", payout["status"])
Authentication
API Key
All requests require a Wave Business API key passed as a Bearer token. Get yours from the Wave Business Portal under Developer → API Keys.
from wave_business_api import WaveClient
client = WaveClient(api_key="wave_sn_prod_YhUNb9d...i4bA6")
Security: Never hardcode your API key. Use environment variables:
import os
from wave_business_api import WaveClient
client = WaveClient(api_key=os.environ["WAVE_API_KEY"])
Request Signing (HMAC-SHA256)
For enhanced security, enable request signing when creating your API key in the Wave Business Portal.
Every request will include a Wave-Signature header with an HMAC-SHA256 signature.
client = WaveClient(
api_key=os.environ["WAVE_API_KEY"],
signing_secret=os.environ["WAVE_SIGNING_SECRET"],
)
Once enabled on an API key, request signing cannot be disabled. If you lose the signing secret, revoke the key and create a new one.
Balance & Reconciliation API
Get Balance
balance = client.balance.get()
# {"amount": "10245", "currency": "XOF"}
# Include subaccount balances
balance = client.balance.get(include_subaccounts=True)
Get Transactions
Returns transactions for a specific day (paginated, up to 1000 per page).
result = client.balance.get_transactions(date="2024-01-15")
print(f"Transactions on {result['date']}:")
for tx in result["items"]:
print(f" {tx['transaction_id']}: {tx['amount']} {tx['currency']}")
# Handle pagination manually
if result["page_info"]["has_next_page"]:
next_page = client.balance.get_transactions(
date="2024-01-15",
after=result["page_info"]["end_cursor"],
)
Iterate All Transactions
Automatically handles pagination and yields every transaction for a given day:
for tx in client.balance.get_all_transactions(date="2024-01-15"):
print(tx["transaction_id"], tx["amount"], tx.get("counterparty_name"))
Refund a Transaction
client.balance.refund_transaction("T_VZSWJF5MMQ")
# Returns None on success; raises WaveAPIError on failure
Checkout API
Create a Checkout Session
session = client.checkout.create_session(
amount="2500", # Amount in smallest currency unit (string)
currency="XOF",
success_url="https://myapp.com/payment/success",
error_url="https://myapp.com/payment/error",
client_reference="order-42", # Optional: your internal reference
restrict_payer_mobile="+221555110219", # Optional: restrict to one phone
)
# Redirect the user to this URL to open the Wave app
print(session["wave_launch_url"])
print(session["id"]) # cos-18qq25rgr100a
Important: Open
wave_launch_urldirectly in the browser. Do not wrap it in a webview.
Get a Checkout Session
session = client.checkout.get_session("cos-18qq25rgr100a")
print(session["checkout_status"]) # "open" | "complete" | "expired"
print(session["payment_status"]) # "processing" | "cancelled" | "succeeded"
Get Session by Transaction ID
session = client.checkout.get_session_by_transaction("TCN4Y4ZC3FM")
Search Sessions
Search by your client_reference:
sessions = client.checkout.search_sessions("order-42")
for s in sessions:
print(s["id"], s["payment_status"])
Expire a Session
Prevent further payment attempts on an open session:
client.checkout.expire_session("cos-18qq25rgr100a")
Refund a Checkout
client.checkout.refund_session("cos-18qq25rgr100a")
# Idempotent: calling twice does not create a duplicate refund
Payout API
Send a Single Payout
Synchronous — Wave attempts the transfer immediately and returns the result. Always provide (or let the SDK auto-generate) an idempotency key.
payout = client.payout.create(
currency="XOF",
receive_amount="15000", # Net amount recipient receives
mobile="+221555110219",
name="Moustapha Mbaye", # Optional: for verification
payment_reason="Salary Jan", # Optional: shown in app (max 40 chars)
client_reference="salary-jan-001",
)
if payout["status"] == "succeeded":
print(f"Sent! Payout ID: {payout['id']}, fee: {payout['fee']}")
elif payout["status"] == "failed":
error = payout.get("payout_error", {})
print(f"Failed: {error.get('error_code')} — {error.get('error_message')}")
Retry with same idempotency key
my_key = "my-unique-idempotency-key-001"
payout = client.payout.create(
currency="XOF",
receive_amount="5000",
mobile="+221555110219",
idempotency_key=my_key,
)
# Safe to retry on network errors — same key = no duplicate
payout = client.payout.create(
currency="XOF",
receive_amount="5000",
mobile="+221555110219",
idempotency_key=my_key, # same key
)
Get a Payout
payout = client.payout.get("pt-185b5e4b8100c")
print(payout["status"]) # "processing" | "succeeded" | "failed" | "reversed"
Search Payouts
payouts = client.payout.search("salary-jan-001")
for p in payouts:
print(p["id"], p["status"])
Create a Payout Batch
Asynchronous — submit many payouts at once and poll for results.
batch = client.payout.create_batch([
{"currency": "XOF", "receive_amount": "1000", "mobile": "+221555110219", "name": "Fatou"},
{"currency": "XOF", "receive_amount": "2000", "mobile": "+221555110233", "name": "Moustapha"},
{"currency": "XOF", "receive_amount": "3000", "mobile": "+221555144081", "name": "Mame"},
])
batch_id = batch["id"]
print(f"Batch submitted: {batch_id}")
Get a Payout Batch
Poll until status == "complete":
import time
while True:
batch = client.payout.get_batch(batch_id)
if batch["status"] == "complete":
break
print("Still processing...")
time.sleep(3)
for p in batch["payouts"]:
if p["status"] == "succeeded":
print(f"✓ {p['mobile']} received {p['receive_amount']}")
elif p["status"] == "failed":
err = p.get("payout_error", {})
print(f"✗ {p['mobile']} failed: {err.get('error_code')}")
Reverse a Payout
Reverse a payout within 3 days of creation:
client.payout.reverse("pt-185b5e4b8100c")
# Idempotent: reversing an already reversed payout returns success
Verify a Recipient
Check a recipient exists and the amount is within their limits before sending:
result = client.payout.verify_recipient(
mobile="+221555110219",
name="Fatou Ndiaye",
amount="10000",
currency="XOF",
)
# result["name_match"]: "MATCH" | "NO_MATCH" | "NAME_NOT_KNOWN"
# result["within_limits"]: True | False | None (if no amount provided)
# result["national_id_match"]: "MATCH" | "NO_MATCH" | "ID_NOT_KNOWN" (if enabled)
if result.get("within_limits") and result.get("name_match") == "MATCH":
payout = client.payout.create(
currency="XOF",
receive_amount="10000",
mobile="+221555110219",
)
Aggregated Merchants API
Access is limited to selected Wave aggregator partners. Contact your Wave representative to request access.
List Merchants
result = client.aggregated_merchants.list(first=20)
for merchant in result["items"]:
print(merchant["id"], merchant["name"], merchant["is_locked"])
# Automatically paginate through all merchants
for merchant in client.aggregated_merchants.list_all():
print(merchant["name"])
Create a Merchant
merchant = client.aggregated_merchants.create(
name="Fatou's Grocery Store", # Must be unique
business_type="other", # "other" or "fintech"
business_description="Local grocery store in Dakar.",
business_sector="retail",
website_url="https://fatous-grocery.example.com",
manager_name="Fatou Diallo",
business_registration_identifier="RC-DKR-2024-001",
)
print(merchant["id"]) # am-...
print(merchant["is_locked"]) # False (until Wave reviews it)
Get a Merchant
merchant = client.aggregated_merchants.get("am-7lks22ap113t4")
print(merchant["payout_fee_structure_name"]) # "one_percent" or "one_fifty_bps"
print(merchant["checkout_fee_structure_name"])
Update a Merchant
updated = client.aggregated_merchants.update(
merchant_id="am-7lks22ap113t4",
name="Fatou's Super Market",
business_type="other",
business_description="An expanded grocery and household store.",
)
# Raises WaveAuthError if the merchant is locked by Wave
Delete a Merchant
client.aggregated_merchants.delete("am-7lks22ap113t4")
Webhooks
Wave sends HTTP POST requests to your endpoint when events occur. Always verify the signature before processing.
Signing Secret (Recommended)
from wave_business_api import WebhookVerifier, WaveWebhookError
verifier = WebhookVerifier(signing_secret="wave_sn_WHS_...")
Shared Secret
verifier = WebhookVerifier(shared_secret="your_shared_secret")
Flask Integration
from flask import Flask, request, jsonify
from wave_business_api import WebhookVerifier, WaveWebhookError
import os
app = Flask(__name__)
verifier = WebhookVerifier(signing_secret=os.environ["WAVE_WEBHOOK_SECRET"])
@app.route("/webhook/wave", methods=["POST"])
def wave_webhook():
try:
event = verifier.verify_and_parse(
raw_body=request.get_data(as_text=True),
wave_signature=request.headers.get("Wave-Signature"),
)
except WaveWebhookError as e:
app.logger.warning(f"Webhook verification failed: {e}")
return jsonify({"error": "Invalid signature"}), 401
event_type = event["type"]
data = event["data"]
if event_type == "checkout.session.completed":
client_ref = data.get("client_reference")
transaction_id = data.get("transaction_id")
# Fulfill the order...
elif event_type == "checkout.session.payment_failed":
# Notify customer...
pass
elif event_type == "merchant.payment_received":
amount = data.get("amount")
sender = data.get("sender_mobile")
# Update your records...
pass
return jsonify({"status": "ok"}), 200
Django Integration
# views.py
import json
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from wave_business_api import WebhookVerifier, WaveWebhookError
import os
verifier = WebhookVerifier(signing_secret=os.environ["WAVE_WEBHOOK_SECRET"])
@csrf_exempt
def wave_webhook(request):
if request.method != "POST":
return HttpResponse(status=405)
try:
event = verifier.verify_and_parse(
raw_body=request.body.decode("utf-8"),
wave_signature=request.META.get("HTTP_WAVE_SIGNATURE"),
)
except WaveWebhookError:
return HttpResponseForbidden("Invalid signature")
event_type = event["type"]
if event_type == "checkout.session.completed":
# process payment...
pass
return HttpResponse("OK")
FastAPI Integration
from fastapi import FastAPI, Request, HTTPException
from wave_business_api import WebhookVerifier, WaveWebhookError
import os
app = FastAPI()
verifier = WebhookVerifier(signing_secret=os.environ["WAVE_WEBHOOK_SECRET"])
@app.post("/webhook/wave")
async def wave_webhook(request: Request):
raw_body = (await request.body()).decode("utf-8")
wave_signature = request.headers.get("wave-signature")
try:
event = verifier.verify_and_parse(
raw_body=raw_body,
wave_signature=wave_signature,
)
except WaveWebhookError as e:
raise HTTPException(status_code=401, detail=str(e))
event_type = event["type"]
if event_type == "checkout.session.completed":
data = event["data"]
# Fulfill order for data["client_reference"]
return {"status": "ok"}
Event Types
| Event | Description |
|---|---|
checkout.session.completed |
Customer successfully paid a checkout session |
checkout.session.payment_failed |
A checkout payment attempt failed |
b2b.payment_received |
Your business received a B2B payment |
b2b.payment_failed |
A B2B payment to your business failed |
merchant.payment_received |
Your business received a customer payment |
test.test_event |
Manual test event from the Business Portal |
Error Handling
All API errors raise typed exceptions that extend WaveAPIError:
| Exception | HTTP Status | When |
|---|---|---|
WaveAuthError |
401, 403 | Invalid key, revoked key, missing signature |
WaveNotFoundError |
404 | Resource not found |
WaveValidationError |
400, 422 | Invalid request data |
WaveIdempotencyError |
409 | Idempotency key conflict |
WaveRateLimitError |
429 | Too many requests |
WaveServerError |
500, 503 | Wave server error |
WaveWebhookError |
— | Invalid webhook signature |
from wave_business_api import WaveClient
from wave_business_api.exceptions import (
WaveAuthError,
WaveNotFoundError,
WaveRateLimitError,
WaveServerError,
WaveValidationError,
)
import time
client = WaveClient(api_key="...")
try:
payout = client.payout.create(
currency="XOF",
receive_amount="5000",
mobile="+221555110219",
)
except WaveAuthError as e:
print(f"Authentication failed: {e.error_code}")
except WaveValidationError as e:
print(f"Invalid request: {e.message}")
if e.details:
for detail in e.details:
print(f" Field {detail['loc']}: {detail['msg']}")
except WaveRateLimitError:
print("Rate limited — waiting before retry")
time.sleep(5)
except WaveServerError as e:
print(f"Wave server error (retry with same idempotency key): {e}")
except WaveNotFoundError:
print("Resource not found")
All exceptions expose:
.message— human-readable error message.status_code— HTTP status code.error_code— Wave error code string.details— validation error details (if any)
IP Whitelisting
If you enable IP whitelisting in the Wave Business Portal, ensure your server's IP is on the allowlist. For webhooks, whitelist these Wave IP addresses on your firewall:
104.155.43.220/32 34.140.23.175/32 34.22.138.147/32
34.76.157.22/32 34.78.253.137/32 34.79.119.200/32
35.189.207.30/32 35.195.255.192/32 35.205.122.113/32
35.205.190.121/32 35.233.61.130/32 35.240.61.196/32
35.240.75.65/32 35.241.190.127/32 35.241.219.1/32
Testing
# Install dev dependencies
pip install wave-business-api[dev]
# Run the test suite
pytest tests/ -v
# With coverage
pytest tests/ -v --cov=wave_business_api --cov-report=term-missing
Contributing
Contributions are welcome!
- Fork the repository: github.com/daniel10027/wave-business-api
- Create your feature branch:
git checkout -b feature/my-feature - Run tests:
pytest tests/ -v - Submit a pull request
License
MIT License — see LICENSE for details.
Author
Jean Marie Daniel Vianney Guedegbe
Full Stack Python & JavaScript Developer · 6+ years experience
📧 danieldanielguedegbe10027@gmail.com
🌐 me.myoctogone.com
🐙 github.com/daniel10027
💼 linkedin.com/in/daniel-guedegbe
#\x00 \x00w\x00a\x00v\x00e\x00-\x00b\x00u\x00s\x00i\x00n\x00e\x00s\x00s\x00-\x00a\x00p\x00i\x00
\x00
\x00#\x00 \x00w\x00a\x00v\x00e\x00-\x00b\x00u\x00s\x00i\x00n\x00e\x00s\x00s\x00-\x00a\x00p\x00i\x00
\x00
\x00
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wave_business_api-1.0.0.tar.gz.
File metadata
- Download URL: wave_business_api-1.0.0.tar.gz
- Upload date:
- Size: 30.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8915cfb36e35b90f5852ccce626ec9c3063b320a3f810b4342c437224c614ab9
|
|
| MD5 |
253171d2b2bc945457bdc465150799ab
|
|
| BLAKE2b-256 |
83521fdf801baad9574391f6e11fbc98504c12fdc2e7dc459cbae528848fbe0a
|
File details
Details for the file wave_business_api-1.0.0-py3-none-any.whl.
File metadata
- Download URL: wave_business_api-1.0.0-py3-none-any.whl
- Upload date:
- Size: 24.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d28e31e805fff2d2cdb49aa366d4abbe731c23bb7aff1730ba2759b7170daca2
|
|
| MD5 |
ff7d2e7c3ce965c0a7c05e0683913953
|
|
| BLAKE2b-256 |
f2184a074e05a77f2387c7cfd2c53cbec393de9a49172d09702b4558eebca70c
|