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

> Run a full marketplace sale in the sandbox, from creating a seller to watching its share land.

Your customers buy from your marketplace, so the sale is yours: the charge lands in your balance, and each seller's share moves down to it when the charge settles. In this guide you'll create a seller, sell something, take the payment, and watch the seller's share arrive. By the end you'll have run a full destination 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 seller">
    A marketplace seller receives money; it never takes payments of its own. So it only needs the `recipient` configuration, and its onboarding stays short. Naming no payment-accepting capability is what keeps it that way.

    <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": "ada@example.com",
          "display_name": "Ada Stores",
          "country": "NG",
          "entity_type": "individual",
          "configuration": { "recipient": {} }
        }'
      ```

      ```json Response theme={"dark"}
      {
        "id": "acct_Shi4LnKkKht5bmbS",
        "name": "Ada Stores",
        "country": "NG",
        "entity_type": "individual",
        "capabilities": {
          "payouts":     { "status": "active", "requested": true },
          "transfers":   { "status": "active", "requested": true },
          "conversions": { "status": "active", "requested": true }
        },
        "configuration": { "recipient": {} },
        "balance_currencies": [],
        "is_active": true
      }
      ```
    </CodeGroup>

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

    `transfers` is what lets the seller receive its share, and `payouts` is what lets it withdraw. In sandbox both are active immediately; in live they start `pending_review` until a reviewer enables them. See [Capabilities](/connect/capabilities) and [Testing Connect](/connect/testing).
  </Step>

  <Step title="Create something to sell">
    The product is **yours**, not the seller's: you are the merchant of record. Price it in NGN, which settles the same day.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl -X POST https://sandbox-api.bachs.io/v1/products \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Handwoven basket",
          "description": "Sold by Ada Stores on the marketplace.",
          "price": { "currency": "NGN", "amount": "100000.00" }
        }'
      ```

      ```json Response theme={"dark"}
      {
        "id": "prod_a3ef11afb6c446f9ba41",
        "name": "Handwoven basket",
        "price": { "currency": "NGN", "price_type": "fixed", "amount": "100000.00" },
        "status": "active"
      }
      ```
    </CodeGroup>

    Copy the product `id` for the next step.
  </Step>

  <Step title="Create the checkout, naming the seller">
    `transfer_data.destination` is what makes this a destination charge: the sale is yours, and the account you name is paid out of it. `platform_fee` is the part you keep; naming `transfer_data.amount` instead fixes what the seller receives, with your platform keeping the rest. See [Destination charges](/connect/split-payments/destination) for both.

    <CodeGroup>
      ```bash Request theme={"dark"}
      curl -X POST https://sandbox-api.bachs.io/v1/checkout-sessions \
        -H "Authorization: Bearer sk_sandbox_abc123xyz..." \
        -H "Content-Type: application/json" \
        -d '{
          "product_cart": [
            { "product_id": "prod_a3ef11afb6c446f9ba41", "quantity": 1 }
          ],
          "transfer_data": { "destination": "acct_Shi4LnKkKht5bmbS" },
          "platform_fee": "20000.00",
          "customer": { "email": "jane@example.com" },
          "success_url": "https://example.com/thank-you",
          "cancel_url": "https://example.com/checkout"
        }'
      ```

      ```json Response theme={"dark"}
      {
        "checkout_id": "chk_yMpCSunbArKaSnzJ",
        "status": "open",
        "amount": "100000.00",
        "currency": "NGN",
        "platform_fee": "20000.00",
        "checkout_url": "https://sandbox-checkout.bachs.io/c/SfxP7y-gSkzJsZs"
      }
      ```
    </CodeGroup>

    Copy the `checkout_id` and `checkout_url`.

    Out of the `100000.00` the customer pays, `20000.00` is yours and the rest is the seller's.

    <Warning>
      Name only an account you own. Any other id is refused, in sandbox and in live.
    </Warning>
  </Step>

  <Step title="Send the customer to pay">
    In your integration you redirect the customer to `checkout_url` and they pay on the page we host. 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">
    The simulated outcome finalizes a second or two later. Read the checkout back until `status` is `completed`:

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

      ```json Response theme={"dark"}
      {
        "checkout_id": "chk_yMpCSunbArKaSnzJ",
        "status": "completed",
        "payment_status": "succeeded",
        "amount": "100000.00",
        "currency": "NGN"
      }
      ```
    </CodeGroup>

    In your integration you would not poll for this. Subscribe to [checkout.completed](/guides/webhooks/events/checkout-completed) instead and react when it arrives.
  </Step>

  <Step title="Read the split">
    The seller's share moves as a transfer when the charge settles, never sooner. Filter by the seller's account id:

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

      ```json Response theme={"dark"}
      {
        "items": [
          {
            "id": "tr_b13a83695e65423fa8854fab",
            "source": "org_bd31b6b3037d404ebe116ec69d955ee3",
            "destination": "acct_Shi4LnKkKht5bmbS",
            "amount": "100000.000000000000000000",
            "currency": "NGN",
            "status": "paid",
            "kind": "payout",
            "source_charge_id": "ch_ff10c7a1b6b4427caf347d748fd4a183",
            "created_at": "2026-08-12T19:41:37.021329+00:00"
          }
        ],
        "pagination": {
          "next_cursor": null,
          "prev_cursor": null,
          "has_more": false,
          "limit": 50,
          "offset": 0,
          "returned": 1,
          "total": 1
        }
      }
      ```
    </CodeGroup>

    `status` is `paid` once settlement has posted the movement. `source_charge_id` ties the share back to the sale that produced it, which is what you show a seller asking where an amount came from.

    The transfer carries the full `100000.00` the customer paid, not the `80000.00` the seller ends up with: because you named your own cut with `platform_fee`, that cut settles separately, readable at [Platform fees](/connect/platform-fees). Naming `transfer_data.amount` instead would fix the seller's share directly, and the transfer would carry only that. See [Destination charges](/connect/split-payments/destination) for both forms.
  </Step>

  <Step title="Read the seller's balance">
    The share is the seller's own money now, in its own balance. Send `X-Account-Id` to read an account's balance instead of yours:

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

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

    Drop the header to read your own, which went up by `19850.00`. That is the whole sale accounted for:

    | | NGN |
    | - | - |
    | Customer paid | 100000.00 |
    | Seller's share | 80000.00 |
    | Your fee, less our processing fee | 19850.00 |
    | Our processing fee | 150.00 |

    Your `20000.00` fee is what you keep, and our fee comes out of it because the sale is yours. See [Processing fees](/connect/processing-fees).

    <Note>
      The seller held no NGN until this sale. An account holds a currency once money arrives in it.
    </Note>
  </Step>
</Steps>

## What you built

A sale that belongs to your marketplace, split with the seller who fulfilled it, and a transfer you can point at to explain where the money went.

The seller 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, and see [Onboard through the API](/connect/guides/api-onboarding) for collecting it, or [Onboarding](/connect/onboarding) to hand the whole flow to us.

Because the sale is yours, a refund or a lost dispute debits **you**, not the seller. See [Refunds and disputes](/connect/marketplaces/refunds-and-disputes) before you go live.

## Next steps

* [Build a SaaS platform](/build/use-cases/saas-platform), where the account sells instead of you
* [Payouts](/connect/payouts)
* [Destination charges](/connect/split-payments/destination)
* [Platform fees](/connect/platform-fees)
* [Take Connect live](/connect/go-live)


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