Dalipay Python SDK
Official Python SDK for the Dalipay Collections & Disbursements API - trigger mobile money checkout prompts (Tigo Pesa, Airtel Money, HaloPesa, AzamPesa) and send payouts, then track status by polling or via webhooks.
Only call this SDK from your backend - never from a browser, mobile app, or any client the public can inspect. If a key pair is exposed, revoke it immediately from your API keys settings and generate a new one.
Installation
pip install dalipay
Quick Start
Dalipay is self-hosted per merchant/gateway deployment, so there's no single shared API host - pass your gateway's own base URL.
from dalipay import Dalipay
client = Dalipay(
public_key="gw_pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
secret_key="gw_sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
base_url="https://your-gateway-domain/api/v1",
)
collection = client.create_collection(
account_number="0712345678",
amount=1000,
external_id="INV-00123",
customer_name="Asha Mwakasege",
# provider is optional - see "Provider auto-detection" below
)
print(f"UUID: {collection.uuid}")
print(f"Status: {collection.status}") # always "processing" right after creation
Health Check
Dalipay doesn't publish a dedicated health-check endpoint, and every POST endpoint has real side effects (a USSD prompt, a money movement). is_healthy() instead does a read-only, side-effect-free probe: it checks collection status for a randomly generated uuid that's virtually guaranteed not to exist. A 404 proves the base URL is reachable and the key pair was accepted (bad credentials would 401 first); nothing is created and no phone number is contacted.
if not client.is_healthy():
raise RuntimeError("Cannot reach Dalipay gateway")
Test vs. production
Every API key pair is tied to one environment - the environment is determined entirely by which key pair you send, there's no separate flag.
| Key prefix | Behavior |
|---|---|
gw_pk_test_... / gw_sk_test_... |
Collections & settlements are simulated instantly - no real USSD prompt, no real money. |
gw_pk_production_... / gw_sk_production_... |
Real money. Triggers an actual USSD prompt and real settlement. |
Double-check which key pair you're using before testing against create_collection with a real phone number - production keys send a live payment prompt.
Collections
Create a collection
Triggers a mobile money checkout prompt on the customer's phone. The customer confirms or cancels on their device; the final result arrives asynchronously.
collection = client.create_collection(
account_number="0712345678", # Required
amount=1000, # Required
external_id="INV-00123", # Required, your own reference, max 30 chars
provider="Tigo", # Optional: Tigo, Airtel, Halopesa, Azampesa, Mpesa
# - if omitted, guessed from account_number's prefix
currency="TZS", # Optional, defaults to TZS
customer_name="Asha Mwakasege", # Optional
)
print(collection.uuid) # store this - you'll need it to check status
print(collection.reference) # human-friendly gateway reference
Check collection status
Poll this to find out whether the customer confirmed or cancelled the prompt. Webhooks are faster for most integrations - use polling as a fallback or an on-demand "refresh".
collection = client.get_collection_status(collection.uuid)
print(collection.status) # processing, success, or failed
Or block until it resolves (or times out) instead of polling by hand:
result = client.wait_for_collection(collection.uuid, timeout=300, interval=3)
print(result.status) # success, failed, or still "processing" if the timeout elapsed
| Status | Meaning |
|---|---|
processing |
Awaiting customer response |
success |
Payment confirmed & balance credited |
failed |
Cancelled, declined, or expired |
A collection can stay processing for up to a few minutes. If you need a hard cutoff, treat anything still processing after ~5 minutes as likely abandoned, while still honoring a late success if it arrives.
There is no automatic retry: a cancelled, declined, or expired prompt is terminal (failed). To try again, call create_collection again with a fresh external_id.
Provider auto-detection
provider is optional on create_collection and create_disbursement - if you omit it, the SDK guesses it from account_number's prefix using guess_provider:
from dalipay import guess_provider
guess_provider("0755660639") # "Mpesa"
guess_provider("0710000000") # "Tigo"
guess_provider("0730000000") # None - 073/TTCL has no mobile money provider on this API
Tanzanian numbers can be ported between networks, so a prefix doesn't guarantee the actual carrier - this is a convenience default, not a guarantee. If you already know the provider (e.g. the customer selected their network at checkout), pass it explicitly rather than relying on the guess. If the prefix can't be resolved (unrecognized, or 073/TTCL, which isn't one of the five supported providers), create_collection/create_disbursement raise ValueError and you must pass provider yourself.
Disbursements
Send a payout from your gateway balance to a mobile money account.
disbursement = client.create_disbursement(
account_number="0712345678", # Required
amount=5000, # Required, between 1 and 5,000,000
external_id="PAYOUT-00045", # Required, your own reference, max 30 chars
provider="Tigo", # Optional: Airtel, Tigo, Azampesa, Halopesa, Mpesa
# - if omitted, guessed from account_number's prefix
recipient_name="Juma Hassan", # Optional
remarks="Agent commission", # Optional
)
print(disbursement.reference)
print(disbursement.status) # "success" in test mode, "awaiting_approval" in production
In production, the amount + fee is held from your balance immediately and the request waits for platform admin approval.
Check disbursement status
disbursement = client.get_disbursement_status(disbursement.reference)
print(disbursement.status)
| Status | Meaning |
|---|---|
awaiting_approval |
Held, queued for admin review |
success |
Sent to recipient |
failed |
Send failed, balance refunded |
rejected |
Declined by admin, balance refunded |
Webhooks
If you configure a webhook URL in Settings, the gateway sends a signed POST request to it whenever a collection resolves.
from dalipay import verify_webhook, WebhookVerificationError
# In your webhook endpoint
try:
payload = verify_webhook(
body=request.body.decode(),
signature=request.headers["X-Signature"],
callback_secret="your_callback_secret", # from Settings
)
if payload.event == "collection.success":
# Mark the order matching payload.data.external_id as paid
print(f"Collection {payload.data.reference} succeeded!")
elif payload.event == "collection.failed":
print(f"Collection {payload.data.reference} failed")
except WebhookVerificationError as e:
print(f"Invalid webhook: {e}")
Respond with a 2xx status quickly. If your endpoint is slow or unreachable, the gateway logs the delivery attempt but does not currently retry - use status polling as a backstop for critical flows.
Webhook events
| Event | Description |
|---|---|
collection.success |
Collection confirmed and balance credited |
collection.failed |
Collection cancelled, declined, or expired |
Async Support
For async applications (FastAPI, aiohttp, etc.):
from dalipay import AsyncDalipay
async def create_collection():
async with AsyncDalipay(
public_key="gw_pk_test_...",
secret_key="gw_sk_test_...",
base_url="https://your-gateway-domain/api/v1",
) as client:
collection = await client.create_collection(
account_number="0712345678",
amount=1000,
external_id="INV-00123",
)
result = await client.wait_for_collection(collection.uuid)
return result
Error Handling
from dalipay import (
Dalipay,
ValidationError,
AuthenticationError,
PaymentRequiredError,
ForbiddenError,
NotFoundError,
MethodNotAllowedError,
ServerError,
)
try:
collection = client.create_collection(...)
except ValidationError as e:
print(f"Invalid request: {e.message}") # 400
except AuthenticationError:
print("Invalid API key pair") # 401
except PaymentRequiredError as e:
print(f"Insufficient balance: {e.message}") # 402
except ForbiddenError:
print("IP not whitelisted, or KYC required") # 403
except NotFoundError:
print("Unknown collection/disbursement") # 404
except MethodNotAllowedError:
print("Wrong HTTP verb") # 405
except ServerError:
print("Dalipay server error, try again later") # 500
Every exception carries .message (human-readable) and .code (HTTP status).
Supported Providers
Tigo, Airtel, Halopesa, Azampesa, Mpesa
License
MIT
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 dalipay-0.1.0.tar.gz.
File metadata
- Download URL: dalipay-0.1.0.tar.gz
- Upload date:
- Size: 17.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4cf0a50acd9c44c0d2ded156fa5baa41c4ce9637944655809afd53e12a8854fb
|
|
| MD5 |
289718a366f85b9d31b6acafb6fb3a02
|
|
| BLAKE2b-256 |
60f5aaae409869a83a2d2e6b166061b4cd6b419631eb376414ee201979b606d6
|
File details
Details for the file dalipay-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dalipay-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ada7c5bd0bcc798ddb8d8a90357b9a06cf12cb9edb540c1e5c38d8f98c93e5a3
|
|
| MD5 |
c8446c57218e5b15c5a9794de8b79d6f
|
|
| BLAKE2b-256 |
d618a9a6e24e65fd35d9a57ded3feed39e928634b2beba3c4254952d11c528e0
|