Sdks

Python SDK

Complete guide to using the PimaBill Python SDK with sync and async support.

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

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

ExceptionHTTP StatusDescription
BadRequestError400Request body or parameters are invalid
UnauthorizedError401Missing or invalid credentials
NotFoundError404The resource does not exist
ConflictError409Conflicts with the current state (e.g. duplicate email)
ApiErrorAnyBase 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

Copyright © 2026