# Marketplace Connect Quickstart

> Onboard a seller and accept a payment for their product in your marketplace.

- **Bolt handles** seller onboarding, checkout, and payment processing.
- **Your app owns** seller identities, product assignments, and order records.

Source: https://help.boltapp.com/products/connect

<aside className="bolt-quickstart-summary not-prose" aria-labelledby="quickstart-outcome">
  <div className="bolt-quickstart-summary__header">
    <h2 id="quickstart-outcome">
      What you'll build
    </h2>

    <span className="bolt-quickstart-summary__time">
      <Icon name="clock" size="14" aria-hidden="true" />

       Est. 45–60 min
    </span>
  </div>

  <ul className="bolt-quickstart-summary__results">
    <li>
      <Icon name="circle-check" aria-hidden="true" />

      <span>Seller onboarding connected</span>
    </li>

    <li>
      <Icon name="circle-check" aria-hidden="true" />

      <span>Single-seller checkout</span>
    </li>

    <li>
      <Icon name="circle-check" aria-hidden="true" />

      <span>Verified marketplace order</span>
    </li>
  </ul>
</aside>

## Your integration steps [#your-integration-steps]

<nav className="bolt-quickstart-steps not-prose" aria-label="Quickstart steps">
  <ol>
    <li>
      <a href="#1-set-up"><span className="bolt-quickstart-steps__number">1</span><span className="bolt-quickstart-steps__label">Set up</span></a>
    </li>

    <li>
      <a href="#2-onboard-seller"><span className="bolt-quickstart-steps__number">2</span><span className="bolt-quickstart-steps__label">Onboard seller</span></a>
    </li>

    <li>
      <a href="#3-connect-orders"><span className="bolt-quickstart-steps__number">3</span><span className="bolt-quickstart-steps__label">Connect orders</span></a>
    </li>

    <li>
      <a href="#4-add-checkout"><span className="bolt-quickstart-steps__number">4</span><span className="bolt-quickstart-steps__label">Add checkout</span></a>
    </li>

    <li>
      <a href="#5-verify-payment"><span className="bolt-quickstart-steps__number">5</span><span className="bolt-quickstart-steps__label">Verify payment</span></a>
    </li>
  </ol>
</nav>

**This flow:** an existing JavaScript / Node.js marketplace, Bolt Connect SDK 2.0.0 or later, Bolt Payments sandbox, and one seller with zero commission.

## Before you begin [#before-you-begin]

Have these ready before the estimated integration time begins. Seller review and account provisioning are separate.

| Requirement          | What you need                                                                                                                                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Existing marketplace | Seller sign-in, a buyer cart, and saved seller/order records. The examples use Node.js 22 and Express.                                                                                                                         |
| Sandbox access       | Ask Bolt to enable Marketplace Connect, Bolt Payments, custom-cart callbacks, and seller splits. Obtain approved sandbox onboarding details for one seller; do not use invented identity or bank information to bypass review. |

**Platform support**

This guide is written for a JavaScript marketplace. The examples use <InlineBrand name="nodedotjs">Node.js</InlineBrand> and <InlineBrand name="express">Express</InlineBrand>. The order, callback, and webhook steps are plain HTTPS calls to Bolt that you can write in any language, but the seller onboarding steps use the Bolt Connect SDK for JavaScript in both the browser and the server; another server language onboards sellers through the [Merchant Onboarding API](/api-merchant-onboarding/). Sellers who take payments on their own storefronts use [Bolt Connect checkout](https://help.boltapp.com/products/bolt-connect/set-up-bolt-connect-sdk/bolt-connect-checkout/).

<IntegrationTable>
  <IntegrationRow label="Frontend SDKs">
    <IntegrationLink href="https://help.boltapp.com/products/bolt-connect/set-up-bolt-connect-sdk/set-up-boltconnect-sdk/" brand="javascript" title="Bolt Connect SDK: the JavaScript SDK this guide uses">
      Bolt Connect
    </IntegrationLink>
  </IntegrationRow>

  <IntegrationRow label="Backend SDKs">
    <IntegrationLink href="/api-reference/api-quickstart" icon="braces" title="Plain HTTPS calls to the Bolt API, as this guide does">
      REST API
    </IntegrationLink>

    <IntegrationLink href="https://github.com/BoltApp/Bolt-Python-SDK" brand="python" title="Bolt Python SDK: wraps the v1 order token and transaction calls">
      Python
    </IntegrationLink>
  </IntegrationRow>
</IntegrationTable>

## 1. Set up [#1-set-up]

1. Open [sandbox API settings](https://merchant-sandbox.boltapp.com/administration/api-keys#apiKeys) for your **marketplace division**. Copy its API key, publishable key, and [signing secret](https://merchant-sandbox.boltapp.com/administration/api-keys#signingSecrets).
2. In [sandbox Payments](https://merchant-sandbox.boltapp.com/payments), configure Bolt Payments for USD with **Immediate Capture**.
3. Save your app's public HTTPS address in [Trusted Domains](https://merchant-sandbox.boltapp.com/administration/trusted-domains).
4. Install the SDK in your existing app and backend package:

```bash title="Terminal"
npm install @boltpay/bolt-connect-sdk@^2.0.0
```

Configure the server using your existing secrets setup:

```bash title="Server environment"
BOLT_API_KEY=YOUR_SANDBOX_MARKETPLACE_API_KEY
BOLT_PUBLISHABLE_KEY=YOUR_SANDBOX_MARKETPLACE_PUBLISHABLE_KEY
BOLT_SIGNING_SECRET=YOUR_SANDBOX_MARKETPLACE_SIGNING_SECRET
```

### Validate setup [#validate-setup]

Confirm the installed version is `2.0.0` or later, the credentials belong to the marketplace division, and that division has Bolt Payments and seller splits enabled. Only the publishable key may reach the browser.

## 2. Onboard seller [#2-onboard-seller]

Choose one saved seller in your app. Reuse that seller's stable ID throughout onboarding and checkout. The SDK calls it `marketplaceSubMerchantID`; order payloads call it `seller_id`.

Connect these two app-owned adapter operations:

| Operation                            | Required behavior                                                                                            |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `sellers.current(req)`               | Return the authenticated seller's saved `{ id }`. Never accept another seller ID from the browser.           |
| `sellers.saveOnboarding(id, status)` | Persist the server-retrieved status against that seller. Do not treat the browser success event as approval. |

Add this authenticated route to your backend. `requireSeller` is your seller-session middleware:

```javascript title="Seller route"
import { CheckoutHelpers } from '@boltpay/bolt-connect-sdk';

app.get('/api/marketplace/seller', requireSeller, async (req, res, next) => {
  res.set('Cache-Control', 'no-store');
  try {
    const seller = await sellers.current(req);
    if (!seller) return res.status(404).json({ error: 'Seller not found' });
    const info = await CheckoutHelpers.getSellerInfo({
      publishableKey: process.env.BOLT_PUBLISHABLE_KEY,
      marketplaceSubMerchantID: seller.id, isTestEnv: true,
    });
    await sellers.saveOnboarding(seller.id, info.onboardingStatus);
    res.json({ sellerId: seller.id, status: info.onboardingStatus,
      marketplaceKey: process.env.BOLT_PUBLISHABLE_KEY });
  } catch (error) { next(error); }
});
```

Add an onboarding button and status message to your existing seller settings page:

```html title="Seller settings"
<button id="seller-onboard" type="button" disabled>Set up payments</button>
<button id="seller-refresh" type="button">Check onboarding status</button>
<p id="seller-status" role="status"></p>
```

Call this function on page mount with your authenticated JSON `request` helper; call its returned cleanup on unmount. The helper rejects non-2xx responses and returns parsed JSON. Keep one seller onboarding session per page.

```javascript title="seller-onboarding.js"
import { Onboarding } from '@boltpay/bolt-connect-sdk';

export function mountSellerOnboarding({ request }) {
  const button = document.querySelector('#seller-onboard');
  const refresh = document.querySelector('#seller-refresh');
  const status = document.querySelector('#seller-status');
  let active = true, configured = false;
  const show = text => { if (active && status.textContent !== text) status.textContent = text; };
  const check = async () => {
    try {
      const seller = await request('/api/marketplace/seller');
      if (!active) return;
      if (!configured) {
        Onboarding.configure({ publishableKey: seller.marketplaceKey,
          marketplaceSubMerchantID: seller.sellerId, isTestEnv: true }, {
          onSuccess: () => { if (active) check(); },
          onFailure: () => { show('Onboarding could not finish. Check your status before reopening it.'); },
          onNotify: () => { show('Follow the instructions in the onboarding form.'); },
        });
        configured = true;
      }
      button.disabled = seller.status !== 'unconfigured';
      show(seller.status === 'completed' ? 'Seller onboarding complete.' :
        seller.status === 'under_review' ? 'Seller is under review. Check again after review.' :
        'Set up payments to continue.');
    } catch { show('Cannot retrieve seller status. Try checking again.'); }
  };
  button.onclick = async () => {
    button.disabled = true;
    try { await Onboarding.start(); }
    catch { show('Onboarding could not open. Check your status and try again.'); }
  };
  refresh.onclick = check;
  check();
  return () => {
    active = false; button.onclick = null; refresh.onclick = null;
    if (configured) Onboarding.end();
  };
}
```

### Validate seller [#validate-seller]

1. Sign in as your test seller and select **Set up payments**. Complete the sandbox form with the details supplied by Bolt.
2. Select **Check onboarding status**. Confirm your backend saves `completed` for that same seller ID.
3. If the result is `under_review`, wait for review. Do not force it to `completed`. A second seller must not receive this seller's record.

## 3. Connect orders [#3-connect-orders]

Save one unpaid order for a **$10.00 USD physical item**, free shipping, no tax, and no discounts. Assign the item's `seller_id` from your catalog's seller record, not a browser-supplied value:

```javascript title="Checkout snapshot"
const order = {
  id: 'YOUR_SAVED_ORDER_ID', sellerId: 'YOUR_ONBOARDED_SELLER_ID', payment: null,
  cart: {
    order_reference: 'YOUR_SAVED_ORDER_ID', display_id: 'YOUR_SAVED_ORDER_ID',
    currency: 'USD', total_amount: 1000, tax_amount: 0,
    items: [{ reference: 'test-mug', name: 'Test mug', type: 'physical',
      seller_id: 'YOUR_ONBOARDED_SELLER_ID', quantity: 1, unit_price: 1000, total_amount: 1000 }],
  },
};
```

Implement the `orders` adapter using your existing store. These are app functions, not SDK methods:

| Operation                    | Required behavior                                                                                                                                                            |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prepare(req)`               | Load the buyer's cart, price it on the server, resolve its seller from your catalog, and save the snapshot above. Reuse an unchanged unpaid snapshot.                        |
| `find(id)`                   | Return that saved snapshot, or `null`, for signed callbacks.                                                                                                                 |
| `findForShopper(id, req)`    | Return it only for its owning buyer, otherwise `null`.                                                                                                                       |
| `recordPayment(id, payment)` | Atomically save `{ reference, status }`. Replays update the same record; reject another reference for a completed payment and prevent stale state from replacing completion. |

Keep the seller and cart fixed during checkout. The example below uses the **marketplace publishable key**, with the seller assigned through `seller_splits`. Do not replace that key with the seller's storefront key. Zero commission and fixed totals are fixtures, not a production fee policy.

```javascript title="marketplace-checkout.mjs"
import { CheckoutHelpers } from '@boltpay/bolt-connect-sdk';
import { createHmac, timingSafeEqual } from 'node:crypto';

export function createMarketplaceHandlers(orders) {
  for (const name of ['BOLT_API_KEY', 'BOLT_PUBLISHABLE_KEY', 'BOLT_SIGNING_SECRET']) {
    if (!process.env[name]) throw new Error(`Set ${name} in your server environment`);
  }
  async function bolt(path, body) {
    const response = await fetch(`https://api-sandbox.boltapp.com${path}`, {
      method: body ? 'POST' : 'GET',
      headers: { 'X-API-Key': process.env.BOLT_API_KEY, 'Content-Type': 'application/json' },
      ...(body ? { body: JSON.stringify(body) } : {}),
    });
    if (!response.ok) throw new Error(`Bolt returned HTTP ${response.status}`);
    return response.json();
  }
  function signedBody(req) {
    if (!Buffer.isBuffer(req.body)) throw new Error('Bolt routes need the raw request body');
    const expected = createHmac('sha256', process.env.BOLT_SIGNING_SECRET)
      .update(req.body).digest();
    // During rotation, the incoming secret signs the Pending header.
    const valid = ['X-Bolt-Hmac-Sha256', 'X-Bolt-Hmac-Sha256-Pending'].some(name => {
      const received = Buffer.from(req.get(name) || '', 'base64');
      return expected.length === received.length && timingSafeEqual(expected, received);
    });
    if (!valid) return null;
    return JSON.parse(req.body.toString('utf8'));
  }
  const handle = fn => async (req, res, next) => {
    res.set('Cache-Control', 'no-store');
    try { await fn(req, res); } catch (error) { next(error); }
  };
  return {
    createOrder: handle(async (req, res) => {
      const order = await orders.prepare(req);
      if (!order || order.cart.order_reference !== order.id || order.payment?.status === 'completed') {
        return res.status(409).json({ error: 'No unpaid checkout snapshot' });
      }
      if (!order.sellerId || order.cart.items.some(item => item.seller_id !== order.sellerId)) {
        return res.status(409).json({ error: 'Order must belong to one seller' });
      }
      const seller = await CheckoutHelpers.getSellerInfo({
        publishableKey: process.env.BOLT_PUBLISHABLE_KEY,
        marketplaceSubMerchantID: order.sellerId, isTestEnv: true,
      });
      if (seller.onboardingStatus !== 'completed') {
        return res.status(409).json({ error: 'Seller onboarding is not complete' });
      }
      const result = await bolt('/v1/merchant/orders', {
        cart: order.cart, channel: 'browser',
        seller_splits: [{ seller_id: order.sellerId,
          amounts: { merchant_subtotal: order.cart.total_amount, discount_total: 0,
            shipping_total: 0, tax_total: 0, gross_total: order.cart.total_amount },
          marketplace_commission_fee: { bps: 0, flat_cents: 0 } }],
      });
      if (!result.token) throw new Error('No order token returned');
      res.json({ id: order.id, token: result.token });
    }),
    orderStatus: handle(async (req, res) => {
      const order = await orders.findForShopper(req.params.id, req);
      if (!order) return res.status(404).json({ error: 'Order not found' });
      res.json({ id: order.id, payment: order.payment });
    }),
    callbacks: handle(async (req, res) => {
      const message = signedBody(req);
      if (!message) return res.status(401).json({ error: 'Invalid signature' });
      const { event, data } = message;
      let reply;
      if (event === 'order.shipping') {
        reply = { shipping_options: [{ service: 'Free sandbox shipping', cost: 0,
          reference: 'free', signature: 'free-v1' }] };
      } else if (event === 'order.tax') {
        reply = { tax_result: { subtotal_amount: 0 }, taxes_included: false };
      } else if (event === 'order.create') {
        const cart = data?.order?.cart;
        const order = cart?.order_reference ? await orders.find(cart.order_reference) : null;
        if (!order || cart.total_amount?.amount !== order.cart.total_amount ||
            cart.currency?.currency !== order.cart.currency) {
          return res.status(422).json({ event, status: 'failure', errors: [
            { code: 2001, data: [{ reason: 'Unknown or changed order' }] },
          ] });
        }
        reply = { display_id: order.cart.display_id };
      } else {
        return res.status(422).json({ event, status: 'failure', error: {
          code: 2319, message: 'This walkthrough does not enable additional checkout features.',
        } });
      }
      res.json({ event, status: 'success', data: reply });
    }),
    webhooks: handle(async (req, res) => {
      const message = signedBody(req);
      if (!message) return res.status(401).json({ error: 'Invalid signature' });
      if (message.object === 'transaction' && message.data?.reference) {
        const transaction = await bolt(`/v1/merchant/transactions/${encodeURIComponent(message.data.reference)}`);
        // This walkthrough accepts card purchases only. Credits/refunds are separate transactions.
        if (transaction.type !== 'cc_payment') return res.json({ status: 'success' });
        if (transaction.reference !== message.data.reference) throw new Error('Transaction reference mismatch');
        const id = transaction.order?.cart?.order_reference;
        const order = id ? await orders.find(id) : null;
        if (!order) throw new Error('No matching local order for this transaction');
        if (transaction.amount?.amount !== order.cart.total_amount ||
            transaction.amount?.currency !== order.cart.currency) {
          throw new Error('Transaction does not match the saved order');
        }
        await orders.recordPayment(id, { reference: transaction.reference, status: transaction.status });
      }
      res.json({ status: 'success' });
    }),
  };
}
```

Register the signed endpoints before your JSON parser or buyer-login middleware. Register the buyer endpoints after your normal parsers; `requireShopper` and `requireCsrf` are your app's protections:

```javascript title="Express routes"
import express from 'express';
import { createMarketplaceHandlers } from './marketplace-checkout.mjs';
const marketplace = createMarketplaceHandlers(orders);

app.post('/marketplace/callbacks', express.raw({ type: 'application/json', limit: '64kb' }), marketplace.callbacks);
app.post('/marketplace/webhooks', express.raw({ type: 'application/json', limit: '64kb' }), marketplace.webhooks);

app.post('/api/marketplace/order', requireShopper, requireCsrf, marketplace.createOrder);
app.get('/api/marketplace/orders/:id', requireShopper, marketplace.orderStatus);
```

Keep your error handler last: log diagnostic details without secrets, send generic browser errors, and return non-2xx for failed notification processing. Raw-body HMAC checks require the original request bytes.

### Validate orders [#validate-orders]

1. From your authenticated buyer session, request `POST /api/marketplace/order`. Expect an order ID and token, with the seller saved against the order.
2. Confirm the outgoing payload assigns both the item and the split to the onboarded seller. The split is `1000` cents gross with zero commission.
3. Use an unconfigured test seller: expect `409` and no order request to Bolt. As a different buyer, request the saved order status: expect `404`.

## 4. Add checkout [#4-add-checkout]

Add a container and status message to your existing buyer cart page:

```html title="Buyer checkout"
<div id="bolt-checkout"></div>
<p id="bolt-status" role="status"></p>
```

Mount this code with the **marketplace** publishable key and your authenticated JSON request helper. The helper sends CSRF protection for POST and rejects failed responses. Use this as the only Bolt script loader, with one active checkout per page. Call the returned cleanup on unmount:

```javascript title="marketplace-browser.js"
const boltScripts = new Map();

export function mountMarketplaceCheckout({ publishableKey, request }) {
  const status = document.querySelector('#bolt-status');
  const container = document.querySelector('#bolt-checkout');
  const cdn = 'https://connect-sandbox.boltapp.com';
  let active = true;
  let timer;
  function show(message) {
    if (active && status.textContent !== message) status.textContent = message;
  }
  function loadScript(id, src) {
    if (!boltScripts.has(id)) {
      const promise = new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.id = id;
        script.src = src;
        script.dataset.publishableKey = publishableKey;
        script.onload = resolve;
        script.onerror = () => {
          script.remove();
          boltScripts.delete(id);
          reject(new Error('Bolt script could not load'));
        };
        document.head.append(script);
      });
      boltScripts.set(id, promise);
    }
    return boltScripts.get(id);
  }
  async function start() {
    show('Preparing checkout…');
    try {
      const { id, token } = await request('/api/marketplace/order', { method: 'POST' });
      if (!active) return;
      container.dataset.tid = 'instant-bolt-checkout-button';
      const button = document.createElement('object');
      button.data = `${cdn}/v1/checkout_button?publishable_key=${encodeURIComponent(publishableKey)}`;
      button.setAttribute('aria-label', 'Open Bolt Checkout');
      container.replaceChildren(button);
      await loadScript('bolt-track', `${cdn}/track.js`);
      if (!active) return;
      await loadScript('bolt-connect', `${cdn}/connect.js`);
      if (!active) return;
      BoltCheckout.configure({ orderToken: token }, {}, {
        success: (_transaction, callback) => {
          show('Payment submitted. Waiting for server confirmation. Do not pay again.');
          callback();
        },
      });
      show(`Order ${id}: ready for checkout.`);
      async function poll() {
        try {
          const order = await request(`/api/marketplace/orders/${encodeURIComponent(id)}`);
          if (!active) return;
          if (order.payment) {
            show(`Order ${id}: ${order.payment.status}. Transaction: ${order.payment.reference}`);
          }
          if (['completed', 'failed', 'cancelled', 'rejected_irreversible'].includes(order.payment?.status)) return;
          timer = setTimeout(poll, 2000);
        } catch {
          show(`Cannot confirm order ${id} yet. Check this order in your account or contact support before paying again.`);
        }
      }
      poll();
    } catch {
      if (active) container.replaceChildren();
      show('Checkout could not load. Return to your cart and try again.');
    }
  }
  start();
  // Returned immediately, so navigation can cancel pending setup as well as polling.
  return () => { active = false; clearTimeout(timer); container.replaceChildren(); };
}
```

1. Open [Merchant Callbacks](https://merchant-sandbox.boltapp.com/administration/api-keys#merchantCallbacks), select **Universal API**, and register your HTTPS origin followed by `/marketplace/callbacks`.
2. Open [Webhooks](https://merchant-sandbox.boltapp.com/administration/webhooks) and register that origin followed by `/marketplace/webhooks`.
3. Open the buyer cart and select the Bolt checkout button.

### Validate checkout [#validate-checkout]

Confirm the modal shows the expected item, **$10.00 USD**, free shipping, and zero tax. Confirm signed shipping/tax/order callbacks reach your server without shopper-session or CSRF errors. No private API key or signing secret may appear in the browser.

## 5. Verify payment [#5-verify-payment]

1. Pay once with `4111 1111 1111 1111`, expiration `03/30`, and CVV `737`.
2. Wait for the app's server-verified completed payment, not just the browser success event.
3. Open [sandbox Transactions](https://merchant-sandbox.boltapp.com/transactions). Match the transaction reference, order ID, amount, and currency.
4. In your order storage, confirm the same order still belongs to the seller from step 2 and contains the completed payment. Reload the buyer page and inspect that saved record again.

### Validate completion [#validate-completion]

You are done when your marketplace onboards one seller, assigns that seller to the order, and persists a verified card payment. This proves the checkout integration, **not settlement into the seller's bank account**. Confirm payout configuration, fees, and settlement separately before launch.

## Test before launch [#test-before-launch]

Run the [Marketplace Connect test plan](/products/connect-test-plan). Your app still needs production fee/tax/shipping rules, concurrency controls, recovery, fulfillment, and refund handling. Confirm seller review, bank-account setup, and payout reconciliation with Bolt before [production readiness](https://help.boltapp.com/developers/production-readiness-guides/).
