Python SDK
The PimaBill Python SDK provides a fully typed, Pythonic interface to the entire PimaBill API. It supports both synchronous and asynchronous usage, and responses are Pydantic models.
Installation
pip install pimabill
poetry add pimabill
Client Initialization
Synchronous Client
import os
from pimabill import PimaBillClient
client = PimaBillClient(token=os.environ["PIMABILL_API_KEY"])
The client talks to the hosted API (https://api.pimabill.com) unless told otherwise:
from pimabill import PimaBillClient, PimaBillClientEnvironment
sandbox = PimaBillClient(token="sk_test_...", environment=PimaBillClientEnvironment.SANDBOX) # https://sandbox.pimabill.com
local = PimaBillClient(token="sk_live_...", environment=PimaBillClientEnvironment.LOCAL) # http://localhost:4000
self_hosted = PimaBillClient(token="sk_live_...", base_url="https://billing.example.com")
Async Client
import os
from pimabill import AsyncPimaBillClient
client = AsyncPimaBillClient(token=os.environ["PIMABILL_API_KEY"])
Every method takes keyword arguments; IDs in the path come first, positionally.
Customers
Customers represent the people or businesses you bill. Every subscription, invoice, and payment is linked to a customer. external_id is your own ID for the customer and must be unique.
Create a Customer
customer = client.customers.create(
external_id="usr_12345",
email="john@example.com",
currency="NGN",
name="John Doe",
metadata={"plan": "enterprise"},
)
print(f"Created customer: {customer.id}")
Get a Customer
customer = client.customers.get("cus_x1y2z3a4b5c6")
print(f"Name: {customer.name}")
print(f"Email: {customer.email}")
Update a Customer
updated = client.customers.update(
"cus_x1y2z3a4b5c6",
name="John A. Doe",
metadata={"plan": "enterprise", "notes": "VIP customer"},
)
List Customers
customers = client.customers.list(
page=1,
limit=20,
search="john",
sort_by="createdAt",
sort_order="desc",
)
print(f"Total customers: {customers.meta.total}")
for customer in customers.data:
print(f"{customer.name} ({customer.email})")
Delete a Customer
client.customers.delete("cus_x1y2z3a4b5c6")
Get Customer Subscriptions and Invoices
for sub in client.customers.get_subscriptions("cus_x1y2z3a4b5c6"):
print(f"{sub.id}: {sub.status} - Plan: {sub.plan_id}")
for invoice in client.customers.get_invoices("cus_x1y2z3a4b5c6"):
print(f"{invoice.invoice_number}: {invoice.status} - {invoice.currency} {invoice.amount}")
Plans
Plans define what you sell: the product tiers, billing frequency, and included features. Each plan has a unique code and a price per currency.
Create a Plan
from pimabill import CreatePlanPriceDto
plan = client.plans.create(
name="Pro Plan",
code="pro_monthly",
billing_interval="MONTHLY",
description="Professional plan with advanced features",
features=["Unlimited projects", "Priority support", "Advanced analytics", "API access"],
prices=[
CreatePlanPriceDto(currency="NGN", amount=15000),
CreatePlanPriceDto(currency="USD", amount=29),
],
)
print(f"Created plan: {plan.id}")
Add a Price
price = client.plans.add_price("pln_d7e8f9g0h1i2", currency="KES", amount=3500)
List Plans
for plan in client.plans.list(is_active=True):
print(f"{plan.name}: {plan.code} ({plan.billing_interval})")
Update or Deactivate a Plan
client.plans.update("pln_d7e8f9g0h1i2", name="Pro Plan v2")
client.plans.update("pln_d7e8f9g0h1i2", is_active=False)
Subscriptions
Subscriptions connect customers to plans and run the recurring billing: the current period, the status (ACTIVE, TRIALING, PAUSED, PAST_DUE, CANCELED) and renewals.
Create a Subscription
subscription = client.subscriptions.create(
customer_id="cus_x1y2z3a4b5c6",
plan_id="pln_d7e8f9g0h1i2",
currency="NGN",
trial_days=14,
metadata={"source": "website"},
)
print(f"Status: {subscription.status}")
print(f"Current period ends: {subscription.current_period_end}")
Cancel a Subscription
canceled = client.subscriptions.cancel("sub_j3k4l5m6n7o8", cancel_at="period_end") # or "now"
print(f"Ends at: {canceled.cancel_at}")
Pause and Resume
client.subscriptions.pause("sub_j3k4l5m6n7o8")
client.subscriptions.resume("sub_j3k4l5m6n7o8")
Change Plan
updated = client.subscriptions.change_plan("sub_j3k4l5m6n7o8", new_plan_id="pln_newplan123", prorate=True)
List Subscriptions
subscriptions = client.subscriptions.list(status="ACTIVE", page=1, limit=50)
for sub in subscriptions.data:
print(f"{sub.id}: {sub.customer_id} on {sub.plan_id}")
Invoices
Invoices track what customers owe. They are created from subscriptions automatically, or by you for one-off charges. Statuses: DRAFT, PENDING, PAID, FAILED, CANCELED.
Create an Invoice
from pimabill import InvoiceItemDto
invoice = client.invoices.create(
customer_id="cus_x1y2z3a4b5c6",
currency="NGN",
items=[
InvoiceItemDto(description="Pro Plan - Monthly", quantity=1, unit_amount=15000),
InvoiceItemDto(description="Additional API calls (500)", quantity=1, unit_amount=2500),
],
due_date="2025-02-28T00:00:00Z",
)
print(f"Invoice: {invoice.invoice_number}, total {invoice.currency} {invoice.amount}")
List Invoices
invoices = client.invoices.list(customer_id="cus_x1y2z3a4b5c6", status="PENDING", page=1, limit=10)
for invoice in invoices.data:
print(f"{invoice.invoice_number}: {invoice.status} - {invoice.currency} {invoice.amount}")
Finalize, Void, or Mark as Paid
client.invoices.finalize("inv_p1q2r3s4t5u6")
client.invoices.void("inv_p1q2r3s4t5u6")
client.invoices.mark_paid("inv_p1q2r3s4t5u6")
Get Invoice PDF
with open("invoice.pdf", "wb") as f:
for chunk in client.invoices.get_pdf("inv_p1q2r3s4t5u6"):
f.write(chunk)
Hosted Checkout
checkout = client.invoices.create_checkout("inv_p1q2r3s4t5u6")
print(f"Send the customer to: {checkout.checkout_url}")
Payments
Payments are created when invoices are paid through a payment provider. You can list, retrieve and refund them.
List Payments
payments = client.payments.list(status="SUCCEEDED", page=1, limit=20)
for payment in payments.data:
print(f"{payment.id}: {payment.currency} {payment.amount} - {payment.status}")
Get a Payment
payment = client.payments.get("pay_v1w2x3y4z5a6")
print(f"Amount: {payment.currency} {payment.amount}, status {payment.status}")
Refund a Payment
refunded = client.payments.refund(
"pay_v1w2x3y4z5a6",
amount=15000, # leave out for a full refund
reason="Customer requested plan downgrade",
)
Payment Providers
List Payment Providers
for provider in client.payment_providers.list():
state = "active" if provider.is_active else "inactive"
print(f"{provider.provider_name}: priority {provider.priority}, {state}")
Configure a Payment Provider
provider = client.payment_providers.configure(
provider_name="stripe",
credentials={"secretKey": "sk_live_...", "webhookSecret": "whsec_..."},
priority=1,
)
test = client.payment_providers.test_connection(provider.id)
print("Connected" if test.success else test.message)
Collections, Payouts and Transfers
One-time money movement, separate from invoices. Each call returns a MoneyMovementResponse, whose status moves from pending or processing to succeeded or failed. A stuck movement is one the provider could not confirm: it is re-checked, never resent. Pass idempotency_key so a retried request cannot move money twice. Payers and payees are Party_MobileMoney, Party_Bank, Party_Paypal, Party_YoAccount, Party_Hosted or Party_Crypto.
Collect a Payment
from pimabill import Party_MobileMoney
collection = client.collections.create(
amount="50000",
currency="UGX",
payer=Party_MobileMoney(msisdn="256771234567"),
description="Order #1042",
reference="order-1042",
idempotency_key="order-1042",
)
if collection.payment_url:
print(f"Send the payer to: {collection.payment_url}")
latest = client.collections.refresh(collection.id)
print(latest.status)
Send a Payout
from pimabill import Party_MobileMoney
payout = client.payouts.create(
amount="120000",
currency="UGX",
payee=Party_MobileMoney(msisdn="256771234567", name="Jane Doe"),
description="Commission for March",
idempotency_key="commission-2025-03-jane",
)
if payout.status == "requires_approval":
print("Waiting for approval in the dashboard")
Move Money Between Wallets
transfer = client.transfers.create(from_wallet_id="wal_a1b2c3", to_wallet_id="wal_d4e5f6", amount="20000")
Ledger
for account in client.ledger.list_accounts(currency="UGX"):
print(f"{account.code}: {account.balance}")
health = client.ledger.get_health()
print("Balanced" if health.balanced else [c.name for c in health.checks if not c.ok])
Webhooks
PimaBill sends events (such as invoice.paid or collection.succeeded) to one URL per organisation, signed with your webhook secret.
client.tenants.update_me(webhook_url="https://api.yourapp.com/webhooks/pimabill")
secret = client.tenants.get_me().webhook_secret
logs = client.tenants.get_webhook_logs(direction="outbound", limit=20)
for log in logs.data:
print(f"{log.event} -> {log.status_code} ({log.attempt_count} attempts)")
See the Webhooks guide for verifying signatures.
Error Handling
Failed requests raise an ApiError with the HTTP status and the response body. Statuses the API documents have their own subclasses.
from pimabill import BadRequestError, ConflictError, NotFoundError, PimaBillClient, UnauthorizedError
from pimabill.core.api_error import ApiError
client = PimaBillClient(token="sk_live_...")
try:
client.customers.get("cus_nonexistent")
except NotFoundError:
print("Customer not found")
except UnauthorizedError:
print("Check your API key")
except (BadRequestError, ConflictError) as e:
print(f"Rejected: {e.body}")
except ApiError as e:
print(f"API error {e.status_code}: {e.body}")
Error Types
| Exception | HTTP Status | Description |
|---|---|---|
BadRequestError | 400 | Request body or parameters are invalid |
UnauthorizedError | 401 | Missing or invalid credentials |
NotFoundError | 404 | The resource does not exist |
ConflictError | 409 | Conflicts with the current state (e.g. duplicate email) |
ApiError | Any | Base class for all API errors |
Async Support
AsyncPimaBillClient has the same methods; await them.
import asyncio
from pimabill import AsyncPimaBillClient
async def main() -> None:
client = AsyncPimaBillClient(token="sk_live_...")
customers, plans = await asyncio.gather(
client.customers.list(limit=10),
client.plans.list(),
)
print(f"{customers.meta.total} customers, {len(plans)} plans")
asyncio.run(main())
Pagination
Paginated lists return data and meta (total, page, limit, total_pages).
page = 1
while True:
response = client.customers.list(page=page, limit=100)
for customer in response.data:
print(customer.name)
if page >= response.meta.total_pages:
break
page += 1
Type Hints
Request and response models are importable from pimabill.
from pimabill import CustomerResponse, InvoiceResponse
def describe(customer: CustomerResponse) -> str:
return f"{customer.name or customer.email} ({customer.id})"
def is_overdue(invoice: InvoiceResponse) -> bool:
return invoice.status == "PENDING"
Configuration Options
from pimabill import PimaBillClient
client = PimaBillClient(
token="sk_live_...", # required
base_url="https://billing.example.com", # default: https://api.pimabill.com
timeout=60.0, # seconds, default 60
)
# Per request:
client.customers.list(request_options={"timeout_in_seconds": 10, "max_retries": 0})
Next Steps
- Explore the TypeScript SDK if you work with Node.js.
- Read the Guides for in-depth usage patterns.
- Check the API Reference for all available endpoints.