Bachs Python SDK
A port of the Bachs TypeScript SDK 1.0.0 with the same resources, request semantics and webhook verification.
pip install bachs
Official server-side Python SDK for Bachs, with a sync client (Bachs) and an async client (AsyncBachs) for asyncio or trio. Python 3.10 or newer. Built on httpx and anyio.
import os
from bachs import Bachs
bachs = Bachs(api_key=os.environ["BACHS_API_KEY"], environment="sandbox")
product = bachs.products.create(
{
"name": "Pro Plan",
"price": {"currency": "USD", "amount": "12.00"},
"billing_cycle": {"interval": "month", "frequency": 1},
},
idempotency_key="unique-operation-key-retained-by-your-app",
)
checkout = bachs.checkout_sessions.create(
{
"product_cart": [{"product_id": product["id"]}],
"customer": {"email": "synthetic@example.com", "name": "Synthetic Customer"},
"success_url": "https://your-public-app.example/success",
},
idempotency_key="another-unique-operation-key",
)
# Redirect the customer to checkout["checkout_url"] in your application.
# A redirect alone does not prove payment or establish access rights.
session = bachs.checkout_sessions.get(checkout["checkout_id"])
Configure the SDK on your backend and keep the secret key out of frontend code. Constructing a client sends no request. environment is required; base_url overrides the endpoint for a proxy or a local test server. Pass http_client=httpx.Client(...) to configure proxies or transports; the caller closes a client it supplies. Use with Bachs(...) as bachs: or bachs.close() to release connections the SDK opened.
Async
AsyncBachs has the same resources, methods, arguments and errors; every method is a coroutine.
from bachs import AsyncBachs
async with AsyncBachs(api_key=os.environ["BACHS_API_KEY"], environment="sandbox") as bachs:
payout = await bachs.payouts.get("po_123")
Create one client at application startup and reuse it, so connections are pooled. Pass http_client=httpx.AsyncClient(...) to customize the transport. Cancelling the calling task (for example a request handler whose client disconnected) raises your framework's normal cancellation exception, not a BachsError. A cancelled read sends nothing further. A write cancelled after it was sent may still have reached Bachs, so treat its outcome as unknown and reconcile before repeating it.
Resources
| Resource | Methods |
|---|---|
products |
create, get, list |
checkout_sessions |
create, get |
payments |
get, list |
subscriptions |
get, list, cancel |
customer_portal |
create_session(customer_id) |
balances |
get |
banks |
list, resolve_account |
payout_destinations |
create, get, list |
payout_quotes |
create |
payouts |
create, get, list |
webhooks |
construct_event |
Creation takes (params, *, idempotency_key=None, timeout=None), retrieval takes (id, *, timeout=None, max_retries=None) and listing takes (params=None, *, timeout=None, max_retries=None). Writes do not accept max_retries.
Requests and responses are plain dictionaries typed with TypedDict (see bachs.types), so fields stay in Bachs snake_case, money stays in decimal strings, and the difference between an omitted key and None reaches the API unchanged. Responses are validated against the fields their type promises and returned as received, including fields this SDK version does not know about.
The product, subscription, payout and limitation notes in the TypeScript SDK README apply unchanged.
Webhooks
from bachs import webhooks
event = webhooks.construct_event(raw_body, request_headers, webhook_signing_secret)
# Persist/check event["id"] atomically in your application before applying effects.
raw_body is the request body as bytes or str before JSON parsing, for example await request.body() in Starlette/FastAPI, request.get_data() in Flask or request.body in Django. request_headers is a dict or your framework's request.headers (Starlette/FastAPI, Flask and Django all work); repeated signature headers are rejected. tolerance defaults to 300 seconds. Verification follows the TypeScript SDK: X-Bachs-Signature-V2 is preferred, any matching v1 digest is accepted during rotation, and malformed V2 does not fall back to the legacy headers.
Request behavior and errors
timeout: default 30 seconds for the whole operation, including retries, waits and reading the response. See How the timeout is enforced.max_retries: default 2 additional attempts, for reads only. Network failures and HTTP 429/500/502/503/504 can retry, with backoff around 0.5 s and 1 s and validRetry-Afterhints honored. No retry starts when its wait cannot fit within the remaining timeout.- Writes are sent exactly once, with or without
idempotency_key. The SDK does not generate keys.
| Error | Meaning |
|---|---|
BachsApiError |
Non-2xx HTTP response: status, body, error_code, field_errors, request_id, retry_after |
BachsNetworkError |
No usable response; the original exception is __cause__; response_status if headers arrived |
BachsTimeoutError |
Subclass of BachsNetworkError: the operation exceeded timeout |
BachsResponseError |
Successful HTTP status with unusable JSON or an unsupported shape |
BachsConfigError |
Local misconfiguration or request misuse, raised before sending |
BachsWebhookError |
Webhook failed verification or is not a supported envelope |
Write 5xx responses, network failures and timeouts after sending, and unusable successful write responses set outcome_unknown = True. Do not treat such an error as proof that no operation exists; reconcile before creating a new operation key.
How the timeout is enforced
timeout is a budget for the whole operation, not for each network step. Both clients check it before each attempt, never start a retry whose wait does not fit, and cap every network wait at the time remaining.
AsyncBachsenforces the deadline exactly. The whole operation runs inside a cancel scope, and cancellation interrupts whatever it is waiting on, including a server that has stopped sending halfway through a response.Bachs(sync) can overrun it. A blocking socket operation cannot be interrupted from the same thread, sohttpxgives each network wait (connecting, sending, waiting for headers, reading the body) its own timeout, fixed when the attempt starts. If the server makes some progress and then goes quiet, the next wait still gets that full timeout. Withtimeout=1.0and a server that goes quiet aftertseconds, the sync client gives up at aboutt + 1seconds, so at worst close to 2 s. Measured against a local server that stalls: going quiet at 0.1, 0.5 and 0.9 s, the sync client stopped at 1.22, 1.55 and 1.98 s, andAsyncBachsat 1.04 s each time.
An overrun needs a server or network path that makes progress and then stalls: a slow connection followed by no response, or a response cut off partway. Refused connections, error statuses, dropped connections and a server that never answers fail within the timeout in both clients. If an upper bound matters to you, for example a request handler with its own deadline, use AsyncBachs, or set the sync timeout to half of your limit.
Differences from the TypeScript SDK
- Python naming:
checkout_sessions,customer_portal.create_session,banks.resolve_account,idempotency_key,max_retries,base_url,webhooks.construct_event(..., tolerance=...). timeoutis in seconds (timeoutMsin TypeScript).- There is no
signaloption and noBachsAbortError. WithAsyncBachs, cancel the task instead; the sync client cannot be cancelled from outside. - The sync client's timeout can overrun when a response stalls mid-body (see above). The TypeScript SDK and
AsyncBachsstop at the exact deadline. products.getrejects whitespace-only IDs, like every other retrieval method.- Requests send a
User-Agent: bachs-python/<version>header.
Development
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/ruff check src tests && .venv/bin/mypy && .venv/bin/pytest
tests/ ports the TypeScript SDK's 30 local scenarios. tests/test_async.py runs AsyncBachs on asyncio and trio, checks that it sends exactly the same requests as Bachs for every method, and covers the exact deadline and cancellation (scenario 28). tests/types/ holds static fixtures that mypy checks: each # type: ignore[...] marks an expected error, and strict mode fails if that error stops occurring. GitHub Actions runs these checks on Python 3.10 to 3.14 for every push and pull request.
These are local tests against simulated responses. The request semantics, response checks and webhook verification follow the TypeScript SDK, whose 1.0.0 was accepted against the Bachs sandbox. The Python SDK itself, including its HTTP layer and AsyncBachs, has not yet been run against the live sandbox.
Metadata
Release files for bachs 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| bachs-1.0.1.tar.gz | 38.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bachs-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.7 kB
Release files / bachs-1.0.1.tar.gz
| Download URL | bachs-1.0.1.tar.gz |
|---|---|
| Size | 38.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
99b5bd3315444046a6826d58b2d360456e0e1283c31333972f0e0c12d3638f84
|
|
BLAKE2b-256 checksum How to use checksums |
65238f6a035510f2cfd645b83e3a1dec0a83410860baf8a322f194d3fa839efa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / bachs-1.0.1-py3-none-any.whl
| Download URL | bachs-1.0.1-py3-none-any.whl |
|---|---|
| Size | 28.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a6513edca6cf11b587e58a82e5486240426d3778ef065ba53b54f3917a7c3ce5
|
|
BLAKE2b-256 checksum How to use checksums |
dc9b3a13afe583129df079112dde57f7032222572d7894c5b240cbc617d0fcb3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for bachs 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for bachs-1.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.