> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bachs.io/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Money is always a decimal string at the currency's precision (for example "29.00"), paired with an ISO 4217 currency field. Never use minor units.
> Build against the sandbox first: base URL https://sandbox-api.bachs.io with sk_sandbox_ keys. Production is https://api.bachs.io with sk_live_ keys; going live is a key swap.
> Treat webhooks (for example collection.succeeded) as the source of truth for fulfilment, never client-side events or redirects.
> Subscriptions are created by completing a checkout for a recurring product. There is no direct create-subscription endpoint.
> IDs carry resource prefixes (cust_, prod_, sub_, chk_, inv_, ref_) and timestamps are ISO 8601 UTC.

# Node.js and TypeScript SDK

> Install @bachs/sdk, create a client, send requests, handle errors and uncertain writes, and verify webhook signatures from a Node.js server.

`@bachs/sdk` is the official Bachs library for Node.js and TypeScript servers. It sends authenticated requests, checks and types the responses, retries reads safely, and verifies webhook signatures.

It runs on your server only. Keep your secret API key out of browser code.

## Install

You need Node.js 22 or later.

<CodeGroup>
  ```bash npm theme={"dark"}
  npm install @bachs/sdk
  ```

  ```bash pnpm theme={"dark"}
  pnpm add @bachs/sdk
  ```

  ```bash yarn theme={"dark"}
  yarn add @bachs/sdk
  ```
</CodeGroup>

The package has no runtime dependencies and ships its own TypeScript types. It works with both `import` and `require`.

## Create a client

Create one client and reuse it. Pass your secret key and the environment it belongs to.

```ts lib/bachs.ts theme={"dark"}
import { Bachs } from "@bachs/sdk";

export const bachs = new Bachs({
  apiKey: process.env.BACHS_API_KEY,
  environment: "sandbox",
});
```

`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](/authentication).

Creating a client sends no request, so a wrong key shows up on your first call.

| Option | Type | Default | Description |
| - | - | - | - |
| `apiKey` | `string` | Required | Your secret API key. |
| `environment` | `"sandbox"` \| `"production"` | Required | Which Bachs environment to call. |
| `baseURL` | `string` | Set by `environment` | Send requests to another host, such as a proxy or a local test server. |
| `timeoutMs` | `number` | `30000` | Time allowed for a whole operation, including retries and reading the response. |
| `maxRetries` | `number` | `2` | Extra attempts for reads. Writes are never retried. |

## Send requests

Each resource on the client maps to a group of API endpoints. Methods follow one pattern:

* **Create:** `create(input, options?)`
* **Retrieve:** `get(id, options?)`
* **List:** `list(filters?, options?)`

```ts theme={"dark"}
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.
```

Request and response fields use the same names as the API, in snake\_case, so the [API reference](/api-reference/overview) describes them exactly. Money amounts are decimal strings, such as `"12.00"`, never numbers.

## Resources

| Resource | Methods | API reference |
| - | - | - |
| `products` | `create`, `get`, `list` | [Products](/api-reference/products/object) |
| `checkoutSessions` | `create`, `get` | [Checkout sessions](/api-reference/checkout-sessions/object) |
| `payments` | `get`, `list` | [Payments](/api-reference/payments/object) |
| `subscriptions` | `get`, `list`, `cancel` | [Subscriptions](/api-reference/subscriptions/object) |
| `customerPortal` | `createSession(customerId)` | [Customer portal sessions](/api-reference/customer-sessions/object) |
| `balances` | `get` | [Balance](/api-reference/balance/object) |
| `banks` | `list`, `resolveAccount` | [Payouts](/guides/payouts/payout-using-api) |
| `payoutDestinations` | `create`, `get`, `list` | [Payout destinations](/api-reference/payout-destinations/object) |
| `payoutQuotes` | `create` | [Payouts](/api-reference/payouts/object) |
| `payouts` | `create`, `get`, `list` | [Payouts](/api-reference/payouts/object) |
| `webhooks` | `constructEvent` | [Verify webhooks](#verify-webhooks) |

`subscriptions.cancel(id, input?, options?)` takes `{ cancel_at_period_end: true }` to cancel at the end of the paid period, or `false` to cancel now. See [Manage subscriptions](/guides/subscriptions/manage).

### 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

Do not force unsupported fields through a type cast. The SDK only sends what its types describe.

## 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:

```ts theme={"dark"}
let cursor: string | undefined;

do {
  const page = await bachs.products.list({ limit: 50, cursor });
  for (const product of page.items) {
    console.log(product.id, product.name);
  }
  cursor = page.pagination.next_cursor ?? undefined;
} while (cursor);
```

Payments, subscriptions, payouts and payout destinations page by `limit` and `offset`. Payout destinations are in `destinations`; the others are in `items`.

```ts theme={"dark"}
const page = await bachs.payouts.list({ limit: 20, offset: 40, status_filter: "completed" });
console.log(page.total, page.items.length);
```

See [Pagination](/guides/pagination).

## Request options

Every method takes an options object as its last argument.

| Option | Applies to | Description |
| - | - | - |
| `idempotencyKey` | Writes | Sent as the `Idempotency-Key` header. Store it with the operation so you can repeat the same write safely. See [Idempotency](/guides/idempotency). |
| `timeoutMs` | All | Overrides the client's `timeoutMs` for this call. |
| `maxRetries` | Reads | Overrides the client's `maxRetries` for this call. |
| `signal` | All | An `AbortSignal` to cancel the call. Cancelling stops waiting; it cannot undo a write Bachs already received. |

## Retries and uncertain writes

**Reads** are retried up to `maxRetries` 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 `timeoutMs`.

**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 `outcomeUnknown: 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.

```ts theme={"dark"}
import { BachsApiError, BachsNetworkError, BachsResponseError } from "@bachs/sdk";

try {
  await bachs.payouts.create(input, { idempotencyKey: operation.key });
} catch (err) {
  const uncertain =
    (err instanceof BachsApiError || err instanceof BachsNetworkError || err instanceof BachsResponseError) &&
    err.outcomeUnknown;

  if (uncertain) {
    await markForReconciliation(operation);
  } else {
    throw err;
  }
}
```

## Errors

Every SDK error extends `BachsError`.

| Error | When | Useful fields |
| - | - | - |
| `BachsApiError` | The API returned an error status. | `status`, `errorCode`, `fieldErrors`, `requestId`, `retryAfter`, `outcomeUnknown`, `body` |
| `BachsNetworkError` | The request or response failed in transit. | `outcomeUnknown`, `responseStatus`, `cause` |
| `BachsTimeoutError` | The call ran past `timeoutMs`. Extends `BachsNetworkError`. | Same as `BachsNetworkError` |
| `BachsAbortError` | Your `signal` cancelled the call. Extends `BachsNetworkError`. | Same as `BachsNetworkError` |
| `BachsResponseError` | The API returned success, but the body was not the expected shape. | `status`, `requestId`, `outcomeUnknown`, `body` |
| `BachsConfigError` | The client or a request was set up wrongly, such as a missing `environment`. Nothing was sent. | `message` |
| `BachsWebhookError` | A webhook failed signature or format checks. | `message` |

```ts theme={"dark"}
import { BachsApiError } from "@bachs/sdk";

try {
  await bachs.checkoutSessions.get(checkoutId);
} catch (err) {
  if (err instanceof BachsApiError && err.status === 404) {
    return null;
  }
  throw err;
}
```

`errorCode` is the `error_code` from the response body, and `fieldErrors` lists any problems with individual fields. See [Errors](/errors). Include `requestId` 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.constructEvent` 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.

```ts app/api/webhooks/bachs/route.ts theme={"dark"}
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;
  }

  // Record event.id durably before acting, so a redelivery is not processed twice.
  await handleEvent(event);

  return new Response(null, { status: 200 });
}
```

It checks `X-Bachs-Signature-V2`, accepts any matching signature while you [rotate a secret](/guides/webhooks/overview#rotating-a-signing-secret), and rejects deliveries more than 300 seconds old or ahead. Change the window with a fourth argument, `{ toleranceSeconds }`. You can call it without a client, or as `bachs.webhooks.constructEvent`.

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](/guides/webhooks/overview).

## TypeScript

Request and response types are exported from the package:

```ts theme={"dark"}
import type { BachsOptions, CreateProductRequest, ProductResponse, WriteRequestOptions } from "@bachs/sdk";
```

Responses are checked at runtime against the fields the types promise. Fields the API adds later are kept on the object as `unknown`, so a new field never breaks your build.

## Source code

The SDK is open source under the MIT license.

* [GitHub repository](https://github.com/bachsdev/bachs-node): source, issues and the full README
* [npm package](https://www.npmjs.com/package/@bachs/sdk)
* [Changelog](https://github.com/bachsdev/bachs-node/blob/main/CHANGELOG.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.