Live shopping
Vertical 9:16 video + pinned product cards + in-stream cart + real Stripe Checkout. TikTok-Shop-style mechanics on your own brand.
/app/catalog. Pin products during a live broadcast from /app/streams/<id> → Interactive tab (spotlight + overlay).
On this page
- 1. Mental model
- 2. Seller onboarding (Stripe Connect)
- 3. Push your product catalog
- 4. Going live with a shopping stream
- 5. Pinning a product during the stream
- 6. Real-money checkout
- 7. Inventory countdown & "X left"
- 8. Dropping a discount code mid-stream
- 9. Auction mode (Whatnot-style bidding)
- 10. Replay with timestamped products
- 11. Webhook events
1. Mental model
A live-shopping session is a normal Kardolive live stream plus a layer of commerce primitives. The stream carries audio + video. Two parallel data channels carry product overlays + chat. When a viewer taps "Buy", your backend mints a Stripe Checkout session — payment goes directly to the seller's Stripe account, and you (Kardolive) optionally take a platform fee.
┌────────────────┐ chat WS ┌──────────────┐
│ Your client │ <───────────────> │ Kardolive │
│ (web/mobile) │ prod-pin, heart │ chat server │
│ │ discount-drop └──────────────┘
│ vertical 9:16 │
│ video player │ HLS / WHEP ┌──────────────┐
│ │ <─────────────── │ Kardolive │
│ │ SRS + CDN │ media plane │
└─────┬──────────┘ └──────────────┘
│
│ "Buy" click
▼
┌────────────────┐ POST /v1/me/commerce/checkout ┌──────────────┐
│ Your backend │ ────────────────────────────────> │ Kardolive │
│ (NestJS, etc) │ │ control API │
└────────────────┘ └──────┬───────┘
│
┌────── Stripe Checkout Session URL ──────┘
▼
seller's Stripe account receives funds,
Kardolive deducts platform fee (if configured),
webhook purchase.completed → your-app /api/webhooks/kardolive
your-app deducts inventory + creates the order in your ERP
2. Seller onboarding (Stripe Connect)
Before a seller can take money, they need a Stripe Express account linked to their Kardolive tenant. Your app's backend triggers the onboarding URL and redirects the seller into Stripe's hosted onboarding flow.
// your-app backend — pseudo-NestJS
@Post('sellers/:tenantId/connect-onboarding')
async getOnboardingLink(@Param('tenantId') tenantId: string) {
const r = await fetch('https://api.kardocloud.com/v1/me/commerce/connect/onboarding-link', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.KARDOLIVE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
returnUrl: 'https://your-app.example.com/dashboard?stripe=ok',
refreshUrl: 'https://your-app.example.com/dashboard?stripe=refresh',
}),
});
return r.json(); // { onboardingUrl, expiresAt }
}
Redirect the seller to onboardingUrl. After they finish, Stripe redirects them back to your returnUrl. Then verify status:
const status = await fetch('https://api.kardocloud.com/v1/me/commerce/connect/status', {
headers: { 'Authorization': 'Bearer kl_live_xxx' },
}).then((r) => r.json());
// { onboarded: true, stripeAccountId: "acct_xxx", chargesEnabled: true }
Platform fee. Set STRIPE_PLATFORM_FEE_PCT (e.g. 2.9 for 2.9%) in the Kardolive API env to take a cut from every sale. Default: 0 — seller keeps 100%.
3. Push your product catalog
Kardolive holds a lightweight product catalog — just what's needed to render the in-stream card (name, image, price, stock count, optional MSRP). Your app remains the source of truth. Push the catalog once, then update as inventory changes.
Bulk sync (recommended for initial load)
POST https://api.kardocloud.com/v1/me/catalog/sync
Authorization: Bearer kl_live_xxx
{
"items": [
{
"externalId": "SKU-1234",
"name": "Linen Summer Dress",
"description": "100% European linen, oversized fit",
"imageUrl": "https://your-cdn.example.com/img/sku-1234.webp",
"priceCents": 4900,
"compareAtCents": 7900,
"currency": "USD",
"stock": 24,
"lowStockAt": 5
},
{ "externalId": "SKU-1235", "name": "...", "priceCents": 6200, "stock": 8 }
]
}
→ { "upserted": 2 }
externalId is your own SKU — Kardolive uses it for idempotent upserts.
Re-running the sync updates rows in place; new SKUs get inserted; nothing else changes.
Single product upsert (call as inventory changes)
POST /v1/me/catalog/products
{ "externalId": "SKU-1234", "name": "Linen Dress", "priceCents": 4900, "stock": 17 }
List / get / archive
GET /v1/me/catalog/products?activeOnly=1
GET /v1/me/catalog/products/<id>
DELETE /v1/me/catalog/products/<id> // soft-deletes (active=false)
4. Going live with a shopping stream
The shopping scenario is just a normal Kardolive stream with mode: "LOW_LATENCY"
(1–3 second delivery via LL-HLS). Open the hosted page at
/scenarios/shopping/?room=<streamId>&token=<jwt> — it loads in a phone-shaped
9:16 stage, connects to chat, and exposes the product overlay UI.
// 1. Create the stream from your backend
const stream = await fetch('https://api.kardocloud.com/v1/me/streams', {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Saturday drop', mode: 'LOW_LATENCY' }),
}).then((r) => r.json());
// 2. Mint a publish/view token for the seller's browser
const { token } = await fetch(`https://api.kardocloud.com/v1/me/streams/${stream.id}/sfu-token`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}` },
}).then((r) => r.json());
// 3. Send them to the hosted scenario
const url = `https://kardocloud.com/scenarios/shopping/?room=${stream.id}&token=${token}`;
window.open(url, '_blank');
5. Pinning a product during the stream
Pinning a product makes it appear as a bottom overlay card on every viewer's screen. Pin events ride the chat WebSocket as a typed data channel — they're free, real-time, and scale to any audience size.
// From the seller's client (the host of the room):
chat.publish('prod-pin', { id: 'SKU-1234' });
// Every viewer's client receives:
chat.onData('prod-pin', (payload) => {
showProductCard(payload.id);
});
The shopping scenario already handles this — it pulls the product details from /v1/me/catalog/products/<id> the first time it sees a pin and caches them.
6. Real-money checkout
When a viewer taps "Buy", your client posts to your backend, which calls Kardolive to mint a Stripe Checkout session. Kardolive returns a hosted-checkout URL — you redirect (or open in a new tab) and Stripe handles cards, Apple Pay, Google Pay, etc.
// Server-to-server:
POST https://api.kardocloud.com/v1/me/commerce/checkout
Authorization: Bearer kl_live_xxx
{
"items": [
{ "productId": "prod_clx...", "quantity": 1 },
{ "productId": "prod_cly...", "quantity": 2 }
],
"streamId": "stream_abc",
"customerEmail": "buyer@example.com",
"customerExternalId": "user_42",
"discountCode": "FLASH50",
"successUrl": "https://your-app.example.com/orders/thanks",
"cancelUrl": "https://your-app.example.com/cart"
}
→ { "url": "https://checkout.stripe.com/c/pay/cs_test_...", "sessionId": "cs_test_..." }
After successful payment Stripe sends a webhook to Kardolive, which validates the signature,
marks the order paid in our DB, decrements inventory (firing inventory.depleted
if stock hits zero), and posts a purchase.completed webhook to your endpoint:
POST /api/webhooks/kardolive (your endpoint)
X-Kardolive-Event: purchase.completed
X-Kardolive-Signature: sha256=...
{
"event": "purchase.completed",
"tenantId": "tnt_xxx",
"occurredAt": "2026-05-21T18:32:11.000Z",
"data": {
"orderId": "ord_xxx",
"stripePaymentIntentId": "pi_xxx",
"customerEmail": "buyer@example.com",
"customerExternalId": "user_42",
"streamId": "stream_abc",
"totalCents": 9800,
"currency": "USD",
"items": [
{ "productId": "prod_clx", "name": "Linen Dress", "quantity": 1, "priceCents": 4900 }
]
}
}
Use this to deduct inventory in your ERP, create the order record, kick off fulfillment, send your own confirmation email — whatever your business logic is.
7. Inventory countdown ("Only 3 left")
Live shoppers convert dramatically better when scarcity is visible.
Set stock when you upsert the product and the scenario reads it automatically.
When stock crosses the lowStockAt threshold (default 5), Kardolive fires an
inventory.low webhook and the overlay starts displaying the countdown:
// Product card shows: "Only 3 left! ⚡"
//
// You can also broadcast a real-time pulse to all viewers when a purchase
// drops the stock — it shakes the product card briefly to draw attention.
chat.publish('stock-tick', { productId: 'SKU-1234', remaining: 3 });
8. Dropping a discount code mid-stream
The "drop" pattern: the host announces a code that's valid for the next 10 minutes only. Viewers see a banner, copy the code, paste it in checkout. Kardolive auto-validates expiry and per-customer cap.
// Host clicks "Drop discount" → your backend calls:
POST /v1/me/commerce/discounts/drop/<streamId>
{
"code": "FLASH50",
"percentOff": 50,
"durationSec": 600, // 10-minute window
"maxRedemptions": 100 // first 100 buyers only
}
// Kardolive broadcasts a discount.dropped webhook + the scenario shows
// a banner across the top of every viewer's stream.
At checkout you pass discountCode in the body — Kardolive validates it and applies the discount inside the Stripe Checkout session so the buyer sees the discounted total.
// Sanity-check a code is still valid before showing it to the buyer
GET /v1/me/commerce/discounts/check/FLASH50
→ { "valid": true, "code": "FLASH50", "percentOff": 50, "endsAt": "..." }
9. Auction mode (Whatnot-style bidding)
Convert any product into a time-limited auction. Kardolive tracks bids atomically (no double-bids under load), enforces minimum increments, and auto-extends the timer if a bid lands in the last 10 seconds (anti-snipe).
// Create the auction for a product (server-to-server)
POST /v1/me/commerce/auctions
{
"productId": "prod_clx...",
"streamId": "stream_abc",
"startingBidCents": 5000,
"reservePriceCents": 8000,
"bidIncrementCents": 100,
"durationSec": 180,
"antiSnipeSec": 10
}
→ { "id": "auc_xxx", "endsAt": "...", "status": "LIVE" }
From a viewer's client, place a bid:
POST /v1/me/commerce/auctions/auc_xxx/bid
{ "amountCents": 5100, "bidderExternalId": "user_42", "bidderName": "Maya" }
→ { "bid": {...}, "newEndsAt": "..." }
When the timer expires Kardolive picks the highest bid (above the reserve, if set) and posts:
{
"event": "auction.won",
"data": {
"auctionId": "auc_xxx",
"winnerExternalId": "user_42",
"amountCents": 8200,
"winnerName": "Maya"
}
}
From there your backend mints a Checkout session for that buyer + product at the auction price.
10. Replay with timestamped products
Roughly half of live-shopping sales happen on the replay, not during the broadcast. Kardolive records the stream (when recording is enabled on the mode) and stores the product-pin timeline alongside it. The hosted replay page renders the same product overlay at the exact second each pin happened — viewers can buy from the replay just like they could live.
// VOD URL with synced product overlay
GET /v1/me/streams/<streamId>/recordings
→ [{
"id": "rec_xxx",
"url": "https://kardocloud.com/vod/<key>.m3u8",
"productTimeline": [
{ "ts": 32.5, "productId": "prod_clx" },
{ "ts": 187.0, "productId": "prod_cly" }
]
}]
Send buyers to /scenarios/shopping/?vod=<recordingId> and the player replays the stream + restores the overlay at each timestamp.
11. Webhook events
Register your endpoint and subscribe to the commerce + lifecycle events you need:
| Event | When |
|---|---|
product.upserted | Catalog sync touched a product |
product.archived | Product was soft-deleted |
inventory.low | Stock crossed the lowStockAt threshold |
inventory.depleted | Stock hit 0 |
discount.created | New code was minted |
discount.dropped | Time-limited code dropped during a stream |
purchase.completed | Stripe confirmed payment |
purchase.refunded | Full or partial refund issued |
auction.bid | New bid placed |
auction.reserve_not_met | Auction ended below reserve, no winner |
auction.won | Auction ended with a winner above reserve |
stream.started / stream.ended | Stream lifecycle (also fires for shopping streams) |
recording.ready | Replay file is uploaded and playable |
Next
Catalog & Stripe reference → · Webhooks deep-dive → · Full API reference →