The official Python SDK for the NombaOne subscription-billing API — recurring billing for Nigeria.
Project description
nombaone
The official Python SDK for the Nomba One subscription-billing API — recurring billing for Nigeria over card, direct debit, bank transfer, and more, with dunning that recovers and a ledger that never loses a kobo.
pip install nombaone
Requires Python 3.9+. Sync and async clients, fully typed (ships py.typed), two dependencies (httpx, pydantic).
Quickstart
Grab a sandbox key (nbo_sandbox_…) from the dashboard, set it as NOMBAONE_API_KEY, and you are three objects away from a live subscription:
import os
from nombaone import Nombaone
nombaone = Nombaone(os.environ["NOMBAONE_API_KEY"])
plan = nombaone.plans.create(name="Pro")
price = nombaone.plans.prices.create(
plan.id,
unit_amount_in_kobo=250_000, # ₦2,500.00 per month
interval="month",
)
customer = nombaone.customers.create(email="ada@example.com", name="Ada Lovelace")
# Sandbox: mint a deterministic test card, then subscribe.
method = nombaone.sandbox.create_payment_method(customer_id=customer.id)
subscription = nombaone.subscriptions.create(
customer_id=customer.id,
price_id=price.id,
payment_method_id=method.id,
)
print(subscription.status) # "active"
The client derives the host from your key prefix — nbo_sandbox_… talks to https://sandbox.api.nombaone.xyz, nbo_live_… to https://api.nombaone.xyz. Server-side only; there is no publishable key to leak.
Async
Every method has an async twin on AsyncNombaone with the identical surface:
import asyncio
from nombaone import AsyncNombaone
async def main():
async with AsyncNombaone() as nombaone:
customer = await nombaone.customers.create(email="ada@example.com", name="Ada")
async for c in await nombaone.customers.list():
print(c.email)
asyncio.run(main())
Sandbox first
The sandbox runs the real billing engine. nombaone.sandbox.* gives you the levers to make a month happen in a second:
# A card that declines like a thin balance does — "not yet", not "no".
nombaone.sandbox.create_payment_method(
customer_id=customer.id,
behavior="decline_insufficient_funds", # or success | requires_otp | decline_expired_card | decline_do_not_honor
)
# The test clock: force the next billing cycle through the real engine.
cycle = nombaone.sandbox.advance_cycle(subscription.id)
print(cycle.outcome) # "paid" | "past_due" | …
# Fire a real, signed webhook at your registered endpoints.
nombaone.sandbox.simulate_webhook(type="invoice.payment_failed")
These methods raise locally (before any network call) if used with a live key.
Money is integer kobo
Every amount in the API is an integer in kobo: ₦1.00 == 100. 250_000 is ₦2,500 — not ₦250,000. No floats, no decimal strings, currency is always "NGN". Multiply naira by 100 exactly once, at the edge of your system; every money field is suffixed _in_kobo so a mixup is hard to type.
Pagination
Every list() works three ways:
# One page.
page = nombaone.invoices.list(status="open", limit=50)
page.data
page.pagination.has_more
page.pagination.next_cursor
# Manual paging (cursor threaded for you).
if page.has_next_page():
nxt = page.next_page()
# Or let the SDK walk every page.
for invoice in nombaone.invoices.list(status="open"):
... # every item across every page
# Async:
async for invoice in await nombaone.invoices.list(status="open"):
...
Errors are a feature
Failures raise typed exceptions carrying everything the API said — the stable code to branch on, a hint telling you exactly what to do next, a doc_url into the error reference, per-field details on validation failures, and the request_id to quote to support:
from nombaone import NotFoundError, RateLimitError, ValidationError
try:
nombaone.subscriptions.create(customer_id=cid, price_id=pid)
except ValidationError as err:
print(err.fields) # {"payment_method_id": ["..."]}
except RateLimitError as err:
print(err.retry_after) # seconds
except NotFoundError as err:
print(err.code) # "CUSTOMER_NOT_FOUND"
| Status | Class | Notes |
|---|---|---|
| 400 | BadRequestError |
malformed request |
| 401 | AuthenticationError |
missing/invalid/wrong-environment key |
| 403 | PermissionDeniedError |
missing scope, foreign resource |
| 404 | NotFoundError |
wrong id or wrong environment |
| 409 | ConflictError |
state conflicts, idempotency reuse |
| 422 | ValidationError |
err.fields has the per-field messages |
| 429 | RateLimitError |
retry_after, limit, remaining |
| 5xx | ServerError |
safe to retry (the SDK already did) |
| — | APIConnectionError / APITimeoutError |
transport-level |
All derive from NombaoneError, so except NombaoneError catches anything the SDK raises.
Idempotency & retries
The SDK auto-generates an Idempotency-Key for every POST and reuses it across its automatic retries (network failures, timeouts, 408/429/5xx — 2 retries by default, honoring Retry-After), so a blip can never double-charge. Pass your own key when the operation must stay idempotent across process restarts:
from nombaone import RequestOptions
nombaone.settlements.create_payout(
amount_in_kobo=5_000_000, bank_code="058", account_number="0123456789",
options=RequestOptions(idempotency_key=f"payout-{my_payout.id}"), # ⚠ doubles as the payout's durable merchantTxRef
)
Every method also accepts options=RequestOptions(timeout=…, max_retries=…, headers=…), and every returned object carries obj.last_response (request_id, status_code, headers, http_response).
Webhooks
Verify before you parse, and dedupe on the event id — delivery is at-least-once, never exactly-once. Verification needs only the signing secret (no API key):
from flask import Flask, request
from nombaone import webhooks # standalone helper
app = Flask(__name__)
@app.post("/nombaone/webhooks")
def hook():
event = webhooks.construct_event(
request.get_data(), # the RAW body — never re-serialize
request.headers.get("x-nombaone-signature", ""),
os.environ["NOMBAONE_WEBHOOK_SECRET"], # shown once when you created the endpoint
)
if already_processed(event.event.id): # at-least-once ⇒ dedupe on event.event.id
return "", 200
if event.type == "invoice.paid":
unlock(event.data["reference"])
elif event.type == "invoice.action_required":
send(event.data["checkoutLink"])
elif event.type == "invoice.payment_failed":
note(event.data["reason"])
return "", 200 # respond 2xx fast, work async
Feed it the raw request body (request.get_data() in Flask, await request.body() in FastAPI, request.body in Django) — json.loads + re-serialization changes bytes and breaks the signature. construct_event checks the X-Nombaone-Signature (t=<unix>,v1=<hex>, HMAC-SHA256 over f"{t}.{body}") in constant time, rejects stale timestamps (300s tolerance, configurable), and returns a parsed WebhookEvent. webhooks.generate_test_header(...) lets you unit-test your handler. Manage endpoints via nombaone.webhook_endpoints (create/rotate return the secret exactly once).
The full surface
customers (+credit, discount) · plans (+nested prices) · prices · subscriptions (pause/resume/cancel/resubscribe/change, schedule, dunning, upcoming invoice, events) · invoices · coupons · payment_methods (hosted-checkout cards, virtual accounts) · mandates (NIBSS direct debit) · settlements (escrow, refunds, payouts) · webhook_endpoints (+deliveries, replay) · events (+catalog) · organization (+billing policy) · metrics · sandbox — every operation in the API reference, 1:1.
Worth knowing:
- Mandates are asynchronous. They start
consent_pendingand activate when the customer's bank confirms — listen forpayment_method.updated, don't poll, don't charge early. - Bank transfer is a push rail.
payment_methods.create_virtual_accountissues a NUBAN; collection completes when the transfer arrives and reconciles. past_dueis not canceled. Readsubscriptions.dunning.retrieve()and honorgrace_access_untilbefore cutting anyone off.
Configuration
Nombaone(
api_key, # or NOMBAONE_API_KEY
base_url=..., # override the derived host
timeout=30.0, # per-attempt seconds
max_retries=2, # automatic retry budget
default_headers=...,# sent on every request
http_client=..., # bring your own httpx.Client (tests, proxies)
)
Examples & development
Runnable scripts live in examples/ — quickstart, pagination, the subscription lifecycle, a webhook receiver, and a dunning rehearsal with the test clock. To develop the SDK itself:
uv sync --extra dev
uv run ruff check . && uv run mypy && uv run pytest # lint, type-check, unit + conformance
NOMBAONE_INTEGRATION=1 NOMBAONE_API_KEY=nbo_sandbox_… uv run pytest tests/integration # live suite
The conformance suite guards spec/openapi.json; refresh it with curl -s <host>/v1/openapi.json -o spec/openapi.json.
Requirements & versioning
Python ≥ 3.9. Semantic versioning; the API itself is versioned at /v1 and additive changes never break you. MIT licensed.
Project details
Release history Release notifications | RSS feed
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 nombaone-0.1.0.tar.gz.
File metadata
- Download URL: nombaone-0.1.0.tar.gz
- Upload date:
- Size: 47.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
252aa203a7f19b19f4e7e027efa2cfd7e69fc73376b5a339e91a510f732a6806
|
|
| MD5 |
1387cdff3748bab48492bfb65e774b85
|
|
| BLAKE2b-256 |
fb62edc3ba50b97f771842fbdffd4e118bda40fa4eb34fc1b383f8d3163fd73a
|
File details
Details for the file nombaone-0.1.0-py3-none-any.whl.
File metadata
- Download URL: nombaone-0.1.0-py3-none-any.whl
- Upload date:
- Size: 62.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.27 {"installer":{"name":"uv","version":"0.11.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
903e29e69a239b1528349df91f59d3ba9bb4877573b6e1861c8fac4764b08ff7
|
|
| MD5 |
7e7b8f2bc98b5e89d43c88f876224a94
|
|
| BLAKE2b-256 |
66381183d9030d03096b93b20da48031df0b9dc04909f7b1c59fe063a03618ef
|