Skip to main content

wave-business-api

PyPI version Python 3.8+ License: MIT

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

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_url directly 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!

  1. Fork the repository: github.com/daniel10027/wave-business-api
  2. Create your feature branch: git checkout -b feature/my-feature
  3. Run tests: pytest tests/ -v
  4. 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

Release files for wave-business-api 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wave-business-api 1.0.0
File Size Uploaded
wave_business_api-1.0.0.tar.gz 30.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wave-business-api 1.0.0
File Interpreter ABI Platform
wave_business_api-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 54.9 kB

Release files / wave_business_api-1.0.0.tar.gz

Download URL wave_business_api-1.0.0.tar.gz
Size 30.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8915cfb36e35b90f5852ccce626ec9c3063b320a3f810b4342c437224c614ab9
BLAKE2b-256 checksum
How to use checksums
83521fdf801baad9574391f6e11fbc98504c12fdc2e7dc459cbae528848fbe0a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release files / wave_business_api-1.0.0-py3-none-any.whl

Download URL wave_business_api-1.0.0-py3-none-any.whl
Size 24.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d28e31e805fff2d2cdb49aa366d4abbe731c23bb7aff1730ba2759b7170daca2
BLAKE2b-256 checksum
How to use checksums
f2184a074e05a77f2387c7cfd2c53cbec393de9a49172d09702b4558eebca70c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page