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

# Build a SaaS platform

> Run a full SaaS sale in the sandbox, where your account is the one selling and your cut comes back to you.

Each business on your platform transacts with its own customers, who often do not know you exist. So the account is the merchant of record: the charge is its sale, the money lands in its balance, and your cut comes back to you. In this guide you'll create an account that sells, take a payment as that account, and read your fee back. By the end you'll have run a full direct charge without moving real money.

## Before you start

* A **sandbox API key** (`sk_sandbox_...`). See [Authentication](/authentication).
* The `connect` capability `active` on your account. See [Become a platform](/connect/become-a-platform).

Every request below goes to `https://sandbox-api.bachs.io`. Going live is the same calls against `https://api.bachs.io` with an `sk_live_` key. See [Take Connect live](/connect/go-live).

## Steps

<Steps>
  <Step title="Create the account, as a merchant">
    This account takes its own payments and gets paid out, so it needs both personas. Name `merchant` and `recipient` as keys in `configuration`, and name each capability it needs under the persona it belongs to.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl -X POST https://sandbox-api.bachs.io/v1/accounts \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "Content-Type: application/json" \
        -d '{
          "contact_email": "studio@example.com",
          "display_name": "Bright Studio",
          "country": "NG",
          "entity_type": "individual",
          "configuration": {
            "merchant": {
              "capabilities": {
                "card_collection": { "requested": true },
                "bank_transfer": { "requested": true },
                "mobile_money": { "requested": true }
              }
            },
            "recipient": {
              "capabilities": {
                "payouts": { "requested": true },
                "transfers": { "requested": true }
              }
            }
          }
        }'
      ```

      ```json Response theme={"dark"}
      {
        "id": "acct_neoYPwWLKjICOBnZ",
        "name": "Bright Studio",
        "country": "NG",
        "entity_type": "individual",
        "capabilities": {
          "card_collection": { "status": "active", "requested": true },
          "bank_transfer":   { "status": "active", "requested": true },
          "mobile_money":    { "status": "active", "requested": true },
          "payouts":         { "status": "active", "requested": true },
          "transfers":       { "status": "active", "requested": true }
        },
        "configuration": { "merchant": {}, "recipient": {} },
        "balance_currencies": [],
        "is_active": true
      }
      ```
    </CodeGroup>

    Copy the `id`. Every step below uses it as the account id.

    The response lists exactly the five capabilities named above, one status per capability. In sandbox they are granted immediately; in live they start `pending_review` until a reviewer enables them. See [Capabilities](/connect/capabilities) and [Testing Connect](/connect/testing).
  </Step>

  <Step title="Let it hold the currency it sells in">
    Holding a currency decides what the account settles in. A new account holds only USD. This walkthrough sells a subscription in NGN, so give the account NGN: its takings then stay in NGN instead of converting to USD on the way to the balance. Today holding the price currency is also required to create a recurring checkout at all (an unheld one is refused with `BASE_CURRENCY_NOT_HELD_BY_ORG` until renewals settle to USD like one-time charges do), so an account selling an NGN plan has to hold NGN first either way. Not every currency Bachs collects in can be held as a balance, so check [Balance currencies](/for-you/supported-currencies#balance-currencies) before you build against one.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl -X POST https://sandbox-api.bachs.io/v1/accounts/acct_neoYPwWLKjICOBnZ \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "Content-Type: application/json" \
        -d '{ "balance_currencies": { "NGN": true } }'
      ```

      ```json Response theme={"dark"}
      {
        "id": "acct_neoYPwWLKjICOBnZ",
        "name": "Bright Studio",
        "balance_currencies": ["NGN"]
      }
      ```
    </CodeGroup>

    USD is always held, cannot be turned off, and does not appear in the list. See [Accounts](/connect/accounts#currencies-the-account-holds) for the field, and [Balance currencies](/for-you/supported-currencies#balance-currencies) for what you can ask for.

    <Warning>
      Skip this step and the checkout below is refused with `BASE_CURRENCY_NOT_HELD_BY_ORG`. It is the account's own currencies that decide this, not yours.
    </Warning>
  </Step>

  <Step title="Create the product as the account">
    The account is selling, so the product is the account's. `X-Account-Id` is what makes a call act as the account rather than as you.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl -X POST https://sandbox-api.bachs.io/v1/products \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "X-Account-Id: acct_neoYPwWLKjICOBnZ" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Studio session",
          "description": "A one-hour booking.",
          "price": { "currency": "NGN", "amount": "50000.00" }
        }'
      ```

      ```json Response theme={"dark"}
      {
        "id": "prod_be4d87e4f01a4570a98a",
        "organization_id": "acct_neoYPwWLKjICOBnZ",
        "name": "Studio session",
        "price": { "currency": "NGN", "price_type": "fixed", "amount": "50000.00" },
        "status": "active"
      }
      ```
    </CodeGroup>

    Copy the product `id`. Note `organization_id`: the product belongs to the account.
  </Step>

  <Step title="Create the checkout as the account">
    `X-Account-Id`, on its own, is what makes this a direct charge: the account becomes the merchant of record and the sale lands in its balance. `platform_fee` is your cut, taken out of its proceeds.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl -X POST https://sandbox-api.bachs.io/v1/checkout-sessions \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "X-Account-Id: acct_neoYPwWLKjICOBnZ" \
        -H "Content-Type: application/json" \
        -d '{
          "product_cart": [
            { "product_id": "prod_be4d87e4f01a4570a98a", "quantity": 1 }
          ],
          "platform_fee": "10000.00",
          "customer": { "email": "buyer@example.com" },
          "success_url": "https://example.com/thanks",
          "cancel_url": "https://example.com/cancel"
        }'
      ```

      ```json Response theme={"dark"}
      {
        "checkout_id": "chk_B1Pa8HgYyRNvyvht",
        "status": "open",
        "amount": "50000.00",
        "currency": "NGN",
        "platform_fee": "10000.00",
        "checkout_url": "https://sandbox-checkout.bachs.io/c/oudFvcBOWo5vBPM"
      }
      ```
    </CodeGroup>

    Copy the `checkout_id` and `checkout_url`.

    <Warning>
      This checkout is the **account's**, not yours. Every later read of it needs `X-Account-Id` too. Without the header the answer is `404 Checkout not found`, which reads as though the checkout was never created.
    </Warning>
  </Step>

  <Step title="Send the customer to pay">
    In your integration the account's customer pays on the page we host at `checkout_url`. To finish this guide, open the link and complete the payment there.

    This account is in sandbox, so the payment is simulated, no funds move. See [Sandbox testing](/guides/payments/sandbox-testing).
  </Step>

  <Step title="Confirm the payment landed">
    Read the checkout back until `status` is `completed`. Note the `X-Account-Id`: this is the account's checkout.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl https://sandbox-api.bachs.io/v1/checkout-sessions/chk_B1Pa8HgYyRNvyvht \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "X-Account-Id: acct_neoYPwWLKjICOBnZ"
      ```

      ```json Response theme={"dark"}
      {
        "checkout_id": "chk_B1Pa8HgYyRNvyvht",
        "status": "completed",
        "payment_status": "succeeded",
        "amount": "50000.00",
        "currency": "NGN",
        "platform_fee": "10000.00"
      }
      ```
    </CodeGroup>

    In your integration you would not poll for this. Subscribe to [checkout.completed](/guides/webhooks/events/checkout-completed) instead. A direct charge's event originates with the account, so your webhook endpoint needs `event_source` set to `connect` or `all` to receive it. See [Connect events](/guides/webhooks/overview#connect-events).
  </Step>

  <Step title="Read your cut back">
    Your fee is not a transfer: it settles as its own record once the charge settles, never sooner. Read it from `GET /v1/platform_fees`:

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl "https://sandbox-api.bachs.io/v1/platform_fees?charge=ch_536484a789f84d58840e85ce1e8b1a84" \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..."
      ```

      ```json Response theme={"dark"}
      {
        "items": [
          {
            "id": "pf_dee4d8fea7834083a086",
            "charge": "ch_536484a789f84d58840e85ce1e8b1a84",
            "collected_from": "acct_neoYPwWLKjICOBnZ",
            "earned_by": "org_bd31b6b3037d404ebe116ec69d955ee3",
            "amount": "10000.00",
            "currency": "NGN",
            "amount_refunded": "0.00",
            "refunded": false,
            "created_at": "2026-08-12T20:13:31.238555+00:00"
          }
        ],
        "pagination": {
          "next_cursor": null,
          "prev_cursor": null,
          "has_more": false,
          "limit": 50,
          "offset": 0,
          "returned": 1,
          "total": 1
        }
      }
      ```
    </CodeGroup>

    `collected_from` is the account and `earned_by` is you, because the sale was theirs and the fee is yours. `charge` ties it back to the sale.
  </Step>

  <Step title="Read the account's balance">
    <CodeGroup>
      ```bash Request theme={"dark"}
      curl https://sandbox-api.bachs.io/v1/balances \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "X-Account-Id: acct_neoYPwWLKjICOBnZ"
      ```

      ```json Response theme={"dark"}
      {
        "account_id": "acct_neoYPwWLKjICOBnZ",
        "balances": [
          { "currency": "NGN", "available_balance": "39850.00", "pending_balance": "0.00" },
          { "currency": "USD", "available_balance": "0.00",     "pending_balance": "0.00" }
        ]
      }
      ```
    </CodeGroup>

    The whole sale, accounted for:

    | | NGN |
    | - | - |
    | Customer paid | 50000.00 |
    | The account keeps | 39850.00 |
    | Your platform fee | 10000.00 |
    | Our processing fee | 150.00 |

    The account bears our processing fee because it is the merchant of record. That is the default, and [Processing fees](/connect/processing-fees) covers moving it. Your `10000.00` arrives whole.
  </Step>
</Steps>

## How this differs from a marketplace

The same two calls, with the parties swapped.

| | This page | [Marketplace](/build/use-cases/marketplace) |
| - | - | - |
| Merchant of record | The account | You |
| What makes it so | `X-Account-Id` | `transfer_data.destination` |
| Where the charge lands | The account's balance | Yours |
| Which way the fee moves | Account to you, its own record at `/v1/platform_fees` | You to seller, a transfer with `kind: payout` |
| Who bears a refund or lost dispute | The account | You |
| Who pays our processing fee, by default | The account | You |

If you are still choosing, see [Choose your integration](/connect/choose-your-integration).

## What happens next

The account can withdraw its balance once it has given you a payout destination, which is one of the requirements it still owes. Read what is outstanding from `requirements.currently_due` on the account.

A refund or a lost dispute debits the **account** here, not you. That is the point of this shape, and the reason its onboarding is longer than a marketplace seller's. See [Refunds](/connect/refunds) and [Disputes](/connect/disputes).

## Next steps

* [Direct charges](/connect/split-payments/direct)
* [Platform fees](/connect/platform-fees)
* [Acting as an account](/connect/acting-as-an-account)
* [Take Connect live](/connect/go-live)


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