Live shopping

Vertical 9:16 video + pinned product cards + in-stream cart + real Stripe Checkout. TikTok-Shop-style mechanics on your own brand.

No-code option: Add products and connect Stripe at /app/catalog. Pin products during a live broadcast from /app/streams/<id>Interactive tab (spotlight + overlay).

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:

EventWhen
product.upsertedCatalog sync touched a product
product.archivedProduct was soft-deleted
inventory.lowStock crossed the lowStockAt threshold
inventory.depletedStock hit 0
discount.createdNew code was minted
discount.droppedTime-limited code dropped during a stream
purchase.completedStripe confirmed payment
purchase.refundedFull or partial refund issued
auction.bidNew bid placed
auction.reserve_not_metAuction ended below reserve, no winner
auction.wonAuction ended with a winner above reserve
stream.started / stream.endedStream lifecycle (also fires for shopping streams)
recording.readyReplay file is uploaded and playable

Next

Catalog & Stripe reference → · Webhooks deep-dive → · Full API reference →