Skip to main content
A marketplace is a Galactic Core deployment that aggregates the catalogs of many independent merchants into one storefront — a multi-vendor shop where a single shopper basket can span several sellers. One continuous operator and shopper scenario runs through this page: discovering the combined catalog and narrowing it to a single merchant, checking out a cart that crosses two merchants in one payment, that payment splitting into per-merchant child orders with commission taken out, reading the shopper’s unified cross-merchant profile, and rendering sponsored placements. Two real merchants carry through the whole scenario:
  • Galactic Core Store086e8d7f-b84d-41bc-8acd-3f5740ea61dc (we call it merchant A)
  • Galactic Test Storeef7b07d3-424b-4b03-b1d4-30a2d2073b61 (merchant B)

Operator keys and store keys

Two kinds of API key separate everything on this page:
  • A store key (tybrite_pk_* / tybrite_sk_*) is scoped to one merchant. Catalog reads return only that store’s products and writes act on that store alone, which is what a single merchant’s own storefront uses.
  • An operator key is issued for a marketplace deployment and aggregates across all active merchants in that marketplace. Discovery reads return the combined catalog, each row stamped with its source merchant_store_id, and the operator can run a unified checkout that fans a single basket out to the right merchants.
The operator key comes in both flavours. The publishable operator key (tybrite_pk_*) covers the read surfaces a storefront calls from the browser — discovery, marketplace info, collections, ad slots. The secret operator key (tybrite_sk_*) runs on your server for the unified checkout, which starts a payment, and the unified shopper profile.
Operator keys work on the discovery and marketplace surfaces (products, categories, search, reviews, marketplace info, unified checkout, the unified profile, ad slots, collections, and cross-merchant trending recommendations). Per-merchant surfaces like promotions, gift cards, posts, and per-merchant cart writes reject an operator key with 403 — those belong to a single merchant’s own store key.

SDK calls in marketplace mode

Most standard GC SDK calls work unchanged under an operator key: a marketplace storefront is built from the same client.products / client.search / client.cartWishlist / client.returns as a single-store one. The table below covers what does change, and where a marketplace-only call replaces a standard one. Discovery, cart, and recommendations aggregate automatically; anything tied to one merchant — returns, reviews, promotions, store info — takes that merchant’s storeId; and checkout, the unified profile, collections, and ads exist only in marketplace mode.

The storefront API and the operator back-office

A marketplace has two halves, and only one of them is an API. The storefront and shopper half is the calls a storefront makes to render and transact. The operator back-office half — everything that configures the marketplace — has no public endpoint at all.
  • Storefront API (this page): aggregated discovery, marketplace info, unified split checkout, reading the unified shopper profile, reading curated collections, filling ad slots, firing ad impression and click beacons, and per-merchant returns.
  • Operator and merchant back-office (admin, not the API): curating collections, ad slot, rate-card and booking management, setting commission rules, running payout batches, merchant onboarding and application approval, and a merchant’s Stripe Connect payout onboarding. These live in the operator’s and merchants’ admin.
The dividing line is configuration: who can sell, what commission applies, which ad runs, when a payout pays are back-office; rendering and transacting the storefront is the API.

1. Aggregated discovery — combined, then narrowed

Discovery endpoints called with an operator key return results aggregated across every active merchant. Each product row carries a merchant_store_id identifying the merchant that sells it, so the same listProducts call returns rows from multiple merchants.
Response
Every row names the merchant that sells it through merchant_store_id and merchant_store_name, so a single page of results spans the whole marketplace. storeId narrows discovery to one merchant within it — the same call, returning only that merchant’s rows:
Response
Every row now carries merchant_store_id: ef7b07d3-…, merchant B alone.
storefront.taxonomy.listCategories() returns a deduped union of categories across merchants, and storefront.search.searchProducts({ q: 'wireless headphones' }) returns cross-merchant results — all read-only, all operator-key. Reviews also work cross-merchant: storefront.reviews.listReviews({ productId, storeId }) reads a merchant’s approved reviews (for an operator key, storeId — the merchant who owns the product — is required).
type: 'trending' recommendations called with the operator key return a cross-merchant rail. Each item is stamped with the merchant_store_id that sells it, which is what links a card through to the right merchant’s product.
Response

2. Marketplace info

The marketplace’s own identity and branding — name, logo, tagline, contact, and whether unified checkout and commission are enabled — is what the storefront shell renders from. It requires an operator key; a single-store key gets 403 and calls getStoreInfo instead.
Response
The marketplace, mode, unified_checkout, and commission_enabled flags always come back. The branding fields — name, logo_url, primary_color, tagline, contact, address — are null until the operator sets them in their admin, so a storefront needs fallbacks for their absence.
storeId returns one merchant’s full store info, in the same shape as a single-store getStoreInfo, which is what a per-merchant shop page renders from: storefront.marketplace.getMarketplaceInfo({ storeId: 'ef7b07d3-424b-4b03-b1d4-30a2d2073b61' }). A merchant not active in the marketplace returns 404.
Currency and pricing have different owners in a marketplace. The storefront currency is marketplace-wide, being the operator’s deployment-level default, so the whole aggregated catalog renders in one currency rather than a different one per merchant. Dynamic pricing rules are per-merchant: each merchant authors their own rules for their own products, and the operator cannot set a merchant’s prices. Commission and payout rules are the operator’s, configured in their admin, and separate from both.

3. Unified split checkout — a cart that spans two merchants

A single shopper cart spanning multiple merchants checks out as one order with one payment. Both merchants from step 1 go into a single basket here — items from merchant A (Galactic Core Store) and merchant B (Galactic Test Store) — with each line item naming the merchant it belongs to in merchant_store_id. The marketplace validates the items, reserves stock per merchant, resolves commission per merchant, and starts one payment for the whole basket. It is a server-side call on the secret operator client.
Response
merchant_breakdown has one entry per merchant in the basket, against exactly one client_secret: the shopper pays total_amount once on the storefront, and the marketplace splits the money.

The commission math

The operator earns a commission on each merchant’s slice — around 8% here, set by the commission_rule_id the operator configured in their admin — and the breakdown carries it directly: For each merchant, gross_amount − commission_amount = net_amount (415.80 − 33.26 = 382.54), and the group’s total_amount is the sum of the gross amounts, which is what the shopper pays in one payment. The operator keeps the summed commission and each merchant is owed their summed net.
Stock is reserved at checkout, each merchant slice getting its own reservation_ids, so concurrent shoppers cannot oversell the last units. An item that cannot be held returns 400 insufficient_stock, and no order or payment is created. Marketplace-wide operator promotions apply automatically and are not passed. A payment provider that cannot be reached returns 502 provider_error.

Applying a merchant’s own discount to its slice

The optional discounts array reduces one merchant’s subtotal with that merchant’s own promotion, gift card, or both. The response then carries a non-empty discount_breakdown, an operator_funded_discount total, and a per-merchant discount_amount, with operator-funded promotions attributed back to the operator — which is what identifies who paid for a discount:
The discounts array is keyed by merchant_store_id, and each entry’s promotion_id and gift_card_code apply to that merchant’s portion only. In the response, discount_breakdown gains one entry per discounted merchant, carrying merchant_discount, gift_card_amount, and the operator_funded_discount share; discount_total is the basket-wide reduction; and that merchant’s merchant_breakdown entry shows its discount_amount and reduced gross_amount.
The server is the price authority here, as in the single-store checkout: the request carries identifiers, never amounts. The multi-merchant checkout recomputes every item price from the catalog and resolves discounts itself — a discounts entry only names a promotion_id or gift_card_code, and the server independently computes the discount that promotion or gift card actually grants, the operator-funded share, every gross_amount / discount_amount / merchant_gross / commission, and the single payment total, then charges exactly that. A named discount that does not validate — expired, ineligible, belonging to another merchant — returns 400 discount_invalid rather than being applied. Because the request carries no client-supplied discount_amount or per-item price, a manipulated basket cannot lower what is charged or inflate a merchant’s payout. The resolved breakdown is held server-side and used verbatim to finalize the per-merchant child orders once payment succeeds, so nothing sent after checkout can change it.

4. Fulfillment → per-merchant child orders

The shopper completes the single payment with the client_secret from step 3. There is no fulfill endpoint to call: fulfillment is driven internally off the payment-success event, and the observable outcome is what an integration builds against.
  • The paid order group splits into one child order per merchant, each landing in that merchant’s own order list with its merchant_net_amount and commission_amount, so each merchant fulfills and ships only their own items.
  • The operator’s split ledger records the gross, commission, and net per merchant — the same numbers from the merchant_breakdown — and each merchant’s net is queued for payout to their connected Stripe account.
  • The group flips to paid.
The internal fulfillment step produces the shape below. The storefront never calls it; it is here to show what landed:
Each children entry maps a merchant to the online_order_id created in that merchant’s order list, carrying the same net_amount from the checkout breakdown — 382.54forAand382.54 for A and 137.08 for B, the two nets that summed to $519.62 above.
Payout depends on a connected merchant account, and the warnings array reports the Stripe Connect transfer state per merchant. A merchant who has not finished Stripe Connect payout onboarding — done in the merchant’s admin, not via the API — has no connected payout account, so their net cannot be transferred yet. Their child order is still created and the shopper is still charged; their funds wait until they connect a payout account. An empty warnings array means every merchant’s transfer went through.

5. Unified shopper profile

Once the shopper from step 3 has bought from more than one merchant, one call returns their unified profile: identity, the merchants they have a relationship with, and their full cross-merchant order history. It takes the session token from the shopper’s sign-in as xCustomerToken, and because it reads the shopper’s own data it runs server-side on the secret operator client.
Response
The same shopper may hold a distinct customer record at each merchant, and customer.ids lists all of them. The stores array is the shopper’s per-merchant relationship, carrying preferred_store and spend totals, and orders is their order history across every merchant, newest first.
Returns are per-merchant: a return belongs to the merchant whose order it is, and there is no marketplace-wide return. A shopper lodges and tracks a return against the specific merchant order, so the return call requires that merchant’s storeId, and that merchant approves and processes it in their own admin. Returning customer covers the returns flow.

6. Sponsored placements and curated sections

The marketplace operator monetizes the storefront with sponsored ad slots and merchandises it with curated collections. Both are operator-key and read-only on the render side; the bookings, rate cards, and curation behind them are operator back-office, per the boundary above. The storefront reads the resolved placements and fires beacons.

Filling an ad slot

A named slot — a hero banner, a category strip, search results — returns its active placements. A slot can carry sponsored placements, which are paid, marked sponsored: true, and rendered with their disclosure_label, and curated fallback placements, which the operator hand-picks and which are not labelled. context scopes a slot to a category or search term.
Response
An empty placements array means the slot has no active booking, sponsored or curated, leaving the storefront’s own organic content in its place. A live placement looks like this:

Impression and click beacons

Each rendered placement fires one impression beacon when it becomes visible and a click beacon when the shopper clicks it, both carrying the placement’s booking_id. The response is 202 Accepted, with billable reporting whether the event counted toward billing after de-duplication — informational, and not something the storefront acts on.
billable comes back false, still with recorded: true, when the beacon was de-duplicated or filtered as invalid traffic — a repeat impression in the same session, for example. The field is informational, and rendering does not depend on it.

Curated merchandising collections

The operator’s curated sections (“Trending now”, “Featured shops”) list first, and one section’s members resolve from its slug. Curation itself is operator back-office; the storefront reads the result.
Response
An empty collections array means the operator has not curated any sections yet. Where sections exist, each carries its slug, name, placement_kind, and homepage flags:
One section’s members, by slug:
Response
listMarketplaceCollections({ placementKind: 'merchant' }) filters the section list by kind. An unknown slug on getMarketplaceCollection returns 404.

What’s next