Skip to main content
bachs is the official Bachs library for Python servers. It sends authenticated requests, checks the responses, retries reads safely, and verifies webhook signatures. It has a sync client, Bachs, and an async client, AsyncBachs, with the same methods. It runs on your server only. Keep your secret API key out of browser and mobile code.

Install

You need Python 3.10 or later.
The package is built on httpx and ships type hints for mypy and Pyright.

Create a client

Create one client and reuse it. Pass your secret key and the environment it belongs to.
environment is required. "sandbox" sends requests to https://sandbox-api.bachs.io and "production" sends them to https://api.bachs.io. Use a sandbox key with "sandbox" and a production key with "production". See Authentication. Creating a client sends no request, so a wrong key shows up on your first call. To release connections when you are done, use the client as a context manager (with Bachs(...) as bachs: or async with AsyncBachs(...) as bachs:), or call bachs.close() or await bachs.aclose().

Send requests

Each resource on the client maps to a group of API endpoints. Methods follow one pattern:
  • Create: create(params, *, idempotency_key=None, timeout=None)
  • Retrieve: get(id, *, timeout=None, max_retries=None)
  • List: list(params=None, *, timeout=None, max_retries=None)
With AsyncBachs, every method is a coroutine: await bachs.products.create(...). Requests and responses are dictionaries with the same field names as the API, in snake_case, so the API reference describes them exactly. Money amounts are decimal strings, such as "12.00", never numbers. Leaving a key out and sending None are different, as in the API.

Resources

subscriptions.cancel(id, params=None, ...) takes {"cancel_at_period_end": True} to cancel at the end of the paid period, or False to cancel now. See Manage subscriptions.

Not in the SDK yet

Use the API directly for these. You can call it alongside the SDK with the same key.
  • Connect: connected accounts, onboarding, the X-Account-Id header and split payments
  • Customers, refunds, disputes and conversions
  • Updating, archiving or unarchiving products, and changing a subscription’s seats or plan
  • Virtual accounts, uploads, product groups and account settings
Your type checker reports fields the SDK does not describe. Do not pass them anyway: the SDK sends the dictionary you give it, and the API may reject or ignore unknown fields.

Lists and pagination

List methods return one page, in the same shape as the API response. The SDK does not fetch further pages for you. Products page by cursor. Pass pagination["next_cursor"] as cursor to get the next page:
Payments, subscriptions, payouts and payout destinations page by limit and offset. Payout destinations are in destinations; the others are in items.
See Pagination.

Request options

Every method takes these as keyword arguments after its other arguments. To cancel an AsyncBachs call, cancel its task, for example with asyncio.timeout() or when your framework drops a disconnected request. The cancellation exception reaches your code as usual. Cancelling stops waiting; it cannot undo a write Bachs already received.

Retries and uncertain writes

Reads are retried up to max_retries times after a network failure or an HTTP 429, 500, 502, 503 or 504. The SDK waits about half a second, then a second, and follows any Retry-After header. It stops when the next wait would pass timeout. Writes are sent once, even with an idempotency key. A write can reach Bachs and still fail on the way back, for example when the connection drops or the API returns a 5xx. In those cases the error has outcome_unknown set to True: the payment, payout or product may exist. When a write’s outcome is unknown, do not send it again with a new key. Look it up first, for example in your payout history or by retrieving the checkout session. If it does not exist, repeat the write with the same idempotency key and the same request.
If you cancel an async write after it was sent, treat its outcome as unknown in the same way.
AsyncBachs stops exactly at timeout. The sync client can run past it in one case: when the server sends part of a response and then stops, the last network read waits for its own full timeout, so a call can take up to about twice timeout. If you need a hard limit, use AsyncBachs or set timeout to half of your limit.

Errors

Every SDK error extends BachsError.
error_code is the error_code from the response body, and field_errors lists any problems with individual fields. See Errors. Include request_id when you contact support. Error bodies and customer portal URLs can contain personal data or credentials. Do not log them in full.

Verify webhooks

webhooks.construct_event checks the signature on a delivery and returns the event. Pass the raw request body, before any JSON parsing, the request headers, and your endpoint’s signing secret. The signing secret is not your API key.
It checks X-Bachs-Signature-V2, accepts any matching signature while you rotate a secret, and rejects deliveries more than 300 seconds old or ahead. Change the window with tolerance=, in seconds. You can call it without a client, or as bachs.webhooks.construct_event.
Flask support for request.headers needs bachs 1.0.1 or later. On 1.0.0, pass dict(request.headers).
It does not deduplicate events or run your handlers. Your app stores processed event IDs and returns a 5xx when handling fails, so Bachs retries. See Webhooks.

Types

Request and response types are TypedDicts in bachs.types:
Responses are checked at runtime against the fields the types promise, then returned as plain dictionaries. Fields the API adds later stay in the dictionary, so a new field never breaks your code.

Source code

The SDK is open source under the MIT license.