A Typed Python client for the Pesapal API with sync/async support, order submission, IPN management, and optional CLI tools.
pip install pesapal-client
from pesapal_client.client import PesapalClientV3
client = PesapalClientV3(
consumer_key="your_consumer_key",
consumer_secret="your_consumer_secret",
is_sandbox=True,
)
The IPN is the url to which pesapal will notify you incase there are any status updates on a recently created payment order. The request method should set via the ipn_nofication_type which can either be a GET or POST
Scenario: Registering an IPN for payment callbacks
from pesapal_client.v3.schemas import IPNRegistrationRequest
ipn_request = IPNRegistrationRequest(
url="https://yourdomain.com/pesapal/ipn",
ipn_notification_type="POST"
)
response = client.ipn.register_ipn(ipn_request)
print(response.ipn_id, response.ipn_status_description)
Pesapal allows registration of multiple IPNs and if incase you need to list them. Here is how you do it.
Scenario: Retrieving all registered IPNs
registered_ipns = client.ipn.get_registered_ipns()
for ipn in registered_ipns:
print(ipn.url, ipn.created_date)
Scenario: Customer checks out with a single payment
from uuid import UUID
from pesapal_client.v3.schemas import InitiatePaymentOrderRequest, BillingAddress
payment_request = InitiatePaymentOrderRequest(
id="ORDER-001",
currency="UGX", # ISO 4217 code
amount=50000.0,
description="Order #001 - Online Purchase",
callback_url="https://yourdomain.com/payment/callback",
notification_id=UUID("YOUR_REGISTERED_IPN_ID"),
billing_address=BillingAddress(
email_address="customer@example.com",
first_name="John",
last_name="Doe",
phone_number="+256700000000",
country_code="UG",
line_1="123 Kampala Road"
)
)
response = client.one_time_payment.initiate_payment_order(payment_request)
print("Redirect user to:", response.redirect_url)
Scenario: Checking payment status after customer completes checkout
from pesapal_client.v3.shcemas import PaymentOrderStatusCode
status = client.one_time_payment.get_payment_order_status(
payment_order_tracking_id="TRACKING_ID_FROM_INITIATE"
)
if status.status_code == PaymentOrderStatusCode.COMPLETED:
print("✅ Payment successful. Fulfill the order.")
elif status.status_code == PaymentOrderStatusCode.FAILED:
print("❌ Payment failed. Ask customer to retry.")
elif status.status_code == PaymentOrderStatusCode.REVERSED:
print("Payment has been refunded")
else:
print("ℹ️ Payment is still processing...")
Subscriptions allow recurring payments
from pesapal_client.v3.schemas import InitiateSubscriptionRequest, SubscriptionDetails
subscription_request = InitiateSubscriptionRequest(
id="SUB-001",
currency="USD",
amount=10.0,
description="Monthly Membership",
callback_url="https://yourdomain.com/subscription/callback",
notification_id=UUID("YOUR_REGISTERED_IPN_ID"),
billing_address=BillingAddress(
email_address="subscriber@example.com",
first_name="Alice",
last_name="Smith",
phone_number="+14155552671",
country_code="US",
),
account_number="ACC12345",
subscription_details=SubscriptionDetails(
start_date="01-01-2025",
end_date="01-01-2026",
frequency="MONTHLY"
)
)
response = client.subscription.initiate_subscription(subscription_request)
print("Redirect user to:", response.redirect_url)
If the subscription details(they are optional) are left out, then the customer shall be asked to choose the periods when redirected to the payment form page(redirect url)
Customer requests for a refund
from pesapal_client.v3.schemas import RefundRequest
refund_request = RefundRequest(
confirmation_code="CONFIRMATION_CODE_FROM_SUCCESSFUL_TXN",
amount="1000.00",
username="customer_identity i.e name or email or phone number",
remarks="Customer requested refund"
)
response = client.one_time_payment.initiate_refund(refund_request)
if response.status == 200:
print("✅ Refund initiated:", response.message)
else:
print("❌ Refund failed:", response.message)
All API calls may raise a PesapalException if Pesapal returns an error (even if the HTTP status code is 200 OK but the body indicates a failure).
You should wrap your calls in a try/except block to handle these gracefully.
Remember to also watch for RequestError exceptions for non Pesapal errors i.e Connection failure, Read Timeout
from pesapal_client.exceptions import PesapalException, RequestError
from pesapal_client.v3.schemas import InitiatePaymentOrderRequest, BillingAddress
from uuid import UUID
try:
payment_request = InitiatePaymentOrderRequest(
id="ORDER-ERR-001",
currency="USD",
amount=50.0,
description="Test Order with Error",
callback_url="https://yourdomain.com/payment/callback",
notification_id=UUID("YOUR_REGISTERED_IPN_ID"),
billing_address=BillingAddress(
email_address="invalid-email", # <-- This will trigger validation error
first_name="Jane",
last_name="Doe"
)
)
response = client.one_time_payment.initiate_payment_order(payment_request)
print("Redirect user to:", response.redirect_url)
except RequestError as e:
# Failed to communicate to Pesapal API
print("Pesapal API Error: ", str(e))
except PesapalException as e:
# Pesapal API-specific error
print("❌ Pesapal API Error:", str(e))
except Exception as e:
# Any other unexpected error
print("⚠️ Unexpected Error:", str(e))
This project is licensed under the MIT License - see the LICENSE file for details.