bolt.com is getting a new home → boltapp.com. Click here to transition smoothly to boltapp.com.

Skip to content

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.
View As MarkdownMCP

Your integration steps

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

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

RequirementWhat you need
Existing marketplaceSeller sign-in, a buyer cart, and saved seller/order records. The examples use Node.js 22 and Express.
Sandbox accessAsk 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 Node.js and Express. 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. Sellers who take payments on their own storefronts use Bolt Connect checkout.

Frontend SDKs
Backend SDKs

1. Set up

  1. Open sandbox API settings for your marketplace division. Copy its API key, publishable key, and signing secret.
  2. In sandbox Payments, configure Bolt Payments for USD with Immediate Capture.
  3. Save your app's public HTTPS address in Trusted Domains.
  4. Install the SDK in your existing app and backend package:
Terminal
npm install @boltpay/bolt-connect-sdk@^2.0.0

Configure the server using your existing secrets setup:

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

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

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:

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

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:

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.

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

  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

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:

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:

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

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:

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

  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

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

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:

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, select Universal API, and register your HTTPS origin followed by /marketplace/callbacks.
  2. Open Webhooks and register that origin followed by /marketplace/webhooks.
  3. Open the buyer cart and select the Bolt checkout button.

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

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

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

Run the Marketplace 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.

On this page