Introducing the official Bachs SDKs for Node.js and Python
@bachs/sdk 2.0.0 and bachs 2.0.0 are here: typed clients for Node.js and Python with safe retries, webhook verification and lists that page themselves.
The official Bachs SDKs for Node.js and Python are here. @bachs/sdk 2.0.0 is on npm and bachs 2.0.0 is on PyPI. They handle the parts every integration needs, such as authenticating requests, typed responses, safe retries and webhook signature checks, so you can spend your time on the parts that are yours: orders, access and fulfilment.
This post covers how to install them, take your first payment, and the decisions we made so that code which moves money fails safely.
Key takeaways
- Install with
npm install @bachs/sdkon Node.js 22 or later, orpip install bachson Python 3.10 or later. - Your API key decides the environment, so going live is a key swap, not a code change.
- An official Go SDK is in development.
- Writes are sent once. If a response is lost, the SDK tells you the outcome is unknown instead of guessing, so you never pay out twice.
Install the SDK
npm install @bachs/sdk pip install bachs The Node.js package works with both import and require and has no runtime dependencies. The Python package has a sync client, Bachs, and an async client, AsyncBachs, for asyncio or trio, with the same methods. Both are for servers only, where your secret key lives.
Create a client
Create one client and reuse it:
import { Bachs } from "@bachs/sdk";
export const bachs = new Bachs({ apiKey: process.env.BACHS_API_KEY }); import os
from bachs import Bachs
bachs = Bachs(api_key=os.environ["BACHS_API_KEY"]) There's no environment setting to keep in step with your key. A key that starts with sk_sandbox_ calls the sandbox, and a key that starts with sk_live_ calls production. If you do pass an environment and it contradicts the key, the client refuses to start instead of sending your key to the wrong place.
Take your first payment
Create a product, then a checkout session for it, and send the customer to the checkout link:
const product = await bachs.products.create(
{
name: "Pro Plan",
price: { currency: "USD", amount: "12.00" },
billing_cycle: { interval: "month", frequency: 1 },
},
{ idempotencyKey: "product-pro-monthly" },
);
const checkout = await bachs.checkoutSessions.create(
{
product_cart: [{ product_id: product.id }],
customer: { email: "ada@example.com", name: "Ada Obi" },
success_url: "https://your-app.example/success",
},
{ idempotencyKey: `checkout-${order.id}` },
);
// Redirect the customer to checkout.checkout_url. The same in Python, where request fields are keyword arguments:
product = bachs.products.create(
name="Pro Plan",
price={"currency": "USD", "amount": "12.00"},
billing_cycle={"interval": "month", "frequency": 1},
idempotency_key="product-pro-monthly",
)
checkout = bachs.checkout_sessions.create(
product_cart=[{"product_id": product.id}],
customer={"email": "ada@example.com", "name": "Ada Obi"},
success_url="https://your-app.example/success",
idempotency_key=f"checkout-{order.id}",
) Field names match the API exactly, in snake_case, so the API reference describes every one. Money amounts are decimal strings, such as "12.00", never numbers.
Verify webhooks
Fulfil orders when the webhook arrives, not when the customer's browser comes back. The SDK checks the signature for you. Pass the raw request body, before any JSON parsing, with the request headers and your endpoint's signing secret:
import { webhooks, BachsWebhookError } from "@bachs/sdk";
export async function POST(request: Request) {
const body = await request.text();
let event;
try {
event = webhooks.constructEvent(body, request.headers, process.env.BACHS_WEBHOOK_SECRET!);
} catch (err) {
if (err instanceof BachsWebhookError) {
return new Response("Invalid signature", { status: 400 });
}
throw err;
}
// Handle event.type here.
return new Response(null, { status: 200 });
} It accepts any matching signature while you rotate a secret, and rejects deliveries more than five minutes old. In Python it's webhooks.construct_event, and it works with the request headers from FastAPI, Flask and Django.
Built so money code fails safely
A payments SDK has one job above the others: never leave you unsure whether money moved. A few decisions follow from that.
Writes are sent once
Reads are retried automatically after a network failure or a temporary server error. Writes are not, even with an idempotency key. A write can reach Bachs and still fail on the way back, for example when the connection drops. When that happens, the error says outcomeUnknown: true (outcome_unknown in Python): the payout or checkout may exist.
So the rule is simple. Look the operation up first. If it doesn't exist, send it again with the same idempotency key. Never retry a payout with a new key on a guess.
Two codes, two names
An API error carries two different codes, and mixing them up is a common bug. The SDKs name them apart: statusCode is the HTTP status, such as 404, and errorCode is the Bachs error code, such as "NOT_FOUND". Branch on errorCode; it's stable. In Python they're status_code and error_code.
Nothing secret in your logs
The SDKs log through a logger option in Node.js and the standard bachs logger in Python. API keys, request and response bodies, customer portal URLs and webhook secrets are never logged, at any level.
Lists that walk themselves
listAll in Node.js and list_all in Python go through every item for you, fetching each page only when you reach it, so you can stop early:
for await (const payout of bachs.payouts.listAll({ status_filter: "completed" })) {
console.log(payout.id, payout.amount);
} What the SDKs cover
Both SDKs cover the same resources: products, checkout sessions, payments, subscriptions, customer portal sessions, balances, banks, payout destinations, payout quotes and payouts, plus webhook verification.
Some parts of the API aren't in the SDKs yet, including Connect, customers, refunds, disputes, conversions and virtual accounts. Call the API directly for those, with the same key, alongside the SDK. The Node.js and Python guides list exactly what's included.
What about other languages?
An official Go SDK is in development, and we'll announce it here when it's ready.
Until then, every SDK calls the same API, so anything an SDK does, a plain HTTP request can do too. Developers in the community have also built SDKs for Go, Rust and PHP. Bachs doesn't maintain or review them, so check what a library covers before you depend on it. You'll find them in community projects.
Check the package name when you install. The official Node.js package is @bachs/sdk, and the official Python package is bachs. Community packages called bachs-sdk and bachs-io are unrelated to them.
Upgrading from 1.x
2.0.0 is the version to install. Earlier 1.x builds predate this announcement. In Node.js, the one breaking change is that the HTTP status on errors is now statusCode instead of status. In Python, requests now take keyword arguments and responses are typed objects, so payout.status instead of payout["status"]. See SDK versioning for the details.
Get started
- Create a sandbox key. See authentication.
- Install the SDK for your language and create a client.
- Create a checkout session and handle the webhook.
- When it works in the sandbox, swap in your live key.
Read the SDK docs, or browse the source on GitHub: Node.js and Python.