BillKit Python SDK
Async + sync client for BillKit, the Stripe-Billing-shape multi-tenant SaaS API on Mollie.
Install
pip install billkit-eu
# or
uv add billkit
Requires Python 3.11+.
Quick start
from billkit import BillKit
client = BillKit(api_key="sk_test_...")
customer = client.customers.create(email="ada@example.com", name="Ada Lovelace")
product = client.products.create(
name="Pro",
description="Hosted billing for SaaS",
marketing_features=["Checkout", "Subscriptions"],
)
price = client.prices.create(
product_id=product["id"],
amount_cents=999,
currency="EUR",
interval="month",
trial_days=14,
payment_methods=["creditcard", "directdebit"],
)
session = client.checkout_sessions.create(
customer_id=customer["id"],
price_id=price["id"],
success_url="https://app.example.com/success",
cancel_url="https://app.example.com/cancel",
)
print(session["url"]) # redirect the user here
One-shot (mandate-less) payments
A one-shot is a single charge with no subscription, mandate or renewals: the Stripe PaymentIntent shape, mapped onto Mollie. Create it, redirect to redirect_url, and settle terminal state via the one_shot_payment.succeeded / .failed webhook events.
payment = client.one_shot_payments.create(
customer_id=customer["id"],
amount_cents=2500,
currency="EUR",
method="ideal",
success_url="https://shop.example.com/thanks",
refund_window_days=14, # optional; 0 disables refunds, default is 30
)
print(payment["redirect_url"]) # redirect the payer here
# Later, refund it within its window (omit amount_cents for a full refund):
client.refunds.create(one_shot_payment_id=payment["id"])
# ...or refund part of it. A charge can carry several partials:
client.refunds.create(one_shot_payment_id=payment["id"], amount_cents=500)
Async
from billkit import AsyncBillKit
async with AsyncBillKit(api_key="sk_test_...") as client:
customer = await client.customers.create(email="ada@example.com")
Configuration
from billkit import BillKit, RetryPolicy
client = BillKit(
api_key="sk_test_...", # or set BILLKIT_API_KEY
base_url="https://api.billkit.eu", # override for self-hosted
timeout=30.0, # seconds, or pass httpx.Timeout
retry_policy=RetryPolicy(
max_attempts=5,
max_retry_after_seconds=10.0, # cap 429 Retry-After sleeps
),
)
The SDK auto-generates an Idempotency-Key for every mutating call, so 5xx and short Retry-After 429 retries are safe: the server replays the original response when an earlier attempt completed. Pass idempotency_key= to coalesce retries across process restarts.
Errors
from billkit import BillKit, ResourceMissingError, RateLimitError, BillKitError
client = BillKit(api_key="sk_test_...")
try:
customer = client.customers.retrieve("cus_doesnt_exist")
except ResourceMissingError:
print("Customer is gone")
except RateLimitError as exc:
print(f"Rate limited; retry in {exc.retry_after}s")
except BillKitError as exc:
print(f"BillKit error {exc.status_code}: {exc.message}")
All errors inherit from BillKitError. Subclasses: APIConnectionError, APIError, ServerError, AuthenticationError, PermissionError, ResourceMissingError, InvalidRequestError, ConflictError, RateLimitError.
Logging
The SDK is silent by default. It owns one logger, logging.getLogger("billkit"), with a NullHandler attached, and it never calls basicConfig, never sets a level, and never adds a handler to a logger it doesn't own. Your logging config is yours.
Turn it on from your application:
import logging
logging.basicConfig()
logging.getLogger("billkit").setLevel(logging.DEBUG)
DEBUG:billkit:BillKit request POST https://api.billkit.eu/v1/customers (attempt 1/3)
DEBUG:billkit:BillKit response POST https://api.billkit.eu/v1/customers -> 503 in 84ms (request_id=req_9f2a)
WARNING:billkit:BillKit retrying POST https://api.billkit.eu/v1/customers after HTTP 503 (attempt 1) in 500ms
DEBUG:billkit:BillKit response POST https://api.billkit.eu/v1/customers -> 200 in 91ms (request_id=req_9f2b)
- DEBUG: one line per attempt, one per response (status, elapsed ms,
X-Request-Id; quote that id to support). - WARNING: one line per retry, with the reason and the delay before the next attempt.
Never logged: your API key or the Authorization header; request and response bodies (they carry customer PII); the query string (list filters carry values like email=); only the path is logged. The final failure isn't logged either: it's raised as a typed BillKitError carrying the status, request id and retry-after, and logging it here too would hand you a duplicate you can't suppress.
The logger object is exported if you'd rather wire it up directly:
from billkit import logger
logger.addHandler(my_handler)
Webhook verification
from billkit import WebhookSignature, WebhookVerificationError
# In your FastAPI / Flask / Django handler:
try:
event = WebhookSignature.verify(
payload=request.body,
signature_header=request.headers.get("BillKit-Signature"),
secret=os.environ["BILLKIT_WEBHOOK_SECRET"],
)
except WebhookVerificationError:
return Response(status_code=400)
if event["type"] == "subscription.created":
handle_new_subscription(event["data"])
The verifier enforces a 5-minute timestamp tolerance (replay protection) and constant-time HMAC compare. Pass tolerance_seconds= to customise.
Development
uv sync --all-extras --dev
uv run pytest
uv run ruff check
uv run mypy src
License
Proprietary.
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 billkit_eu-0.1.0.tar.gz.
File metadata
- Download URL: billkit_eu-0.1.0.tar.gz
- Upload date:
- Size: 74.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9443104044c1271a02d92977d2240048358b17e382663c00155fdee1907b68e4
|
|
| MD5 |
a76a6b6afd8f0672377bd609af8c4657
|
|
| BLAKE2b-256 |
2bbc3e8b1145666257e30e78604009e66b055d7106ec4fb3538edc2f9216f1fa
|
Provenance
The following attestation bundles were made for billkit_eu-0.1.0.tar.gz:
Publisher:
publish.yml on billkit-eu/billkit-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
billkit_eu-0.1.0.tar.gz -
Subject digest:
9443104044c1271a02d92977d2240048358b17e382663c00155fdee1907b68e4 - Sigstore transparency entry: 2583369198
- Sigstore integration time:
-
Permalink:
billkit-eu/billkit-python@94ed327707cb8f87f7397ac6b55fd3ce00c20f28 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/billkit-eu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@94ed327707cb8f87f7397ac6b55fd3ce00c20f28 -
Trigger Event:
push
-
Statement type:
File details
Details for the file billkit_eu-0.1.0-py3-none-any.whl.
File metadata
- Download URL: billkit_eu-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8858d8279a0582acb9ec9ac26d62f646d1a68887e8732c229c437e9603f20ee6
|
|
| MD5 |
2f94cbe7c8ed9713094e77b0234658bc
|
|
| BLAKE2b-256 |
e1a57f50469c4eb9b11438425f6fcae7134d12a7f2c5eda4173430c21e347674
|
Provenance
The following attestation bundles were made for billkit_eu-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on billkit-eu/billkit-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
billkit_eu-0.1.0-py3-none-any.whl -
Subject digest:
8858d8279a0582acb9ec9ac26d62f646d1a68887e8732c229c437e9603f20ee6 - Sigstore transparency entry: 2583369212
- Sigstore integration time:
-
Permalink:
billkit-eu/billkit-python@94ed327707cb8f87f7397ac6b55fd3ce00c20f28 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/billkit-eu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@94ed327707cb8f87f7397ac6b55fd3ce00c20f28 -
Trigger Event:
push
-
Statement type: