pesapal-client

Release Build status Commit activity License

A Typed Python client for the Pesapal API with sync/async support, order submission, IPN management, and optional CLI tools.


Features


Installation

pip install pesapal-client

Quick Start

from pesapal_client.client import PesapalClientV3

client = PesapalClientV3(
    consumer_key="your_consumer_key",
    consumer_secret="your_consumer_secret",
    is_sandbox=True,
)

Register IPN(Instant Payment Notification)

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)

Get Registered IPNs

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)

Initiate a Payment / Submit an Order

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)

Checking Payment Status

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

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)


Refunds

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)

Exception Handling

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

Example: Handling Errors During Payment Initiation

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))


License

This project is licensed under the MIT License - see the LICENSE file for details.