Shoplive Developers
한국어KOEnglishEN日本語JA

Shoplive Shortform SDK Integration Guide

This guide walks you through adding commerce shorts to your app, organized by the case you want to build (a mission). Full-screen playback, embedded collections, floating previews, product and share handling — pick only the parts you need.
Switch the code samples with the iOS / Android tabs at the top of each block.

Getting started

Overview & Requirements

The Shoplive Shortform SDK brings commerce-oriented vertical shorts to your app. The SDK owns the whole shorts experience — playback, preloading, infinite scroll, product overlays, likes, comments and bookmarks. Your app only decides which shorts to show (the query) and where a tap should take the user (routing).

Start now →

Module Structure

The editor is a separate SDK Letting users create and upload shorts (UGC) is handled by the Shortform Editor SDK, not this one — see the Shortform Editor guide. This document covers the viewing side only.

Mission map — what do you want to build?

What you wantMission
A button opens shorts full screen, swipeable up and downMission 1 · Open full screen
A collection of shorts inside your home or campaign screenMission 2 · Embed a collection
A small floating window with related shorts on a product pageMission 3 · Floating preview
Signed-in users can like, comment and bookmarkMission 4 · Connect user identity
A product tap opens your own detail page; sharing uses your own linkMission 5 · Events, products, sharing
Play exactly the list and order your server decides (personalized feed)Mission 6 · Play your own list
Hide the like or share button, or change what a card showsMission 7 · Customize the UI

Delivery Forms (Platform Mapping)

FormiOSAndroid
Full-screen playbackShopLiveShortform.play(requestData:)ShopLiveShortform.play(context, collectionData)
Collection (card / vertical / horizontal)CardTypeViewBuilder · ListViewBuilder → getView()ShopLiveShortformCardTypeView and friends, in XML
Full feed inside a screenShopLiveShortsCollectionView (UIView)ShopLiveShortformDetailTypeView (XML view)
Floating previewShopLiveShortform.showPreview(requestData:)ShopLiveShortform.showPreview(activity, data)
Receiving eventsShopLiveShortformReceiveHandlerDelegateShopLiveShortformHandler
The platform difference in one line On iOS you build a view (build(...).getView()); on Android you place a view in XML, configure it, then call submit(). The query options (tags, brands, SKUs, group) and the event model are the same on both sides, so what you learn on one platform carries over to the other.

Requirements

ItemiOSAndroid
Minimum versioniOS 11.0+minSdk 23+ with shoplive-media3 (the default engine) · 19+ with shoplive-exoplayer
Language / toolchainSwift 5Kotlin · JDK 17. compileSdk 36
SDK version1.8.13 (ShopliveShortformSDK)1.8.14 (shoplive-short-form)
Required companion modulesShopliveSDKCommon (common-ios)shoplive-common · shoplive-network · a player engine
Keep the versions in step The shortform SDK and the shared modules are released to be used at the same version. Upgrading only one side can cause missing-symbol failures at runtime.
Getting started

Installation

iOS

// ① Swift Package Manager
// Xcode ▸ File ▸ Add Package Dependencies…
dependencies: [
    .package(url: "https://github.com/shoplive/shortform-ios", .upToNextMajor(from: "1.8.13")),
    .package(url: "https://github.com/shoplive/common-ios",    .upToNextMajor(from: "1.8.13")),
]

// ② CocoaPods — Podfile
// pod 'ShopliveShortformSDK', '1.8.13'
// pod 'ShopliveSDKCommon',    '1.8.13'

// ③ Import
import ShopliveSDKCommon        // access key, authentication, common models
import ShopLiveShortformSDK     // shorts playback, lists, preview
// settings.gradle / build.gradle — mavenCentral() required
dependencies {
    implementation("cloud.shoplive:shoplive-short-form:1.8.14")   // shorts
    implementation("cloud.shoplive:shoplive-common:1.8.14")       // shared (access key, auth)
    implementation("cloud.shoplive:shoplive-network:1.8.14")      // networking

    // Playback engine — declare <b>only one</b> of these (engine versions are numbered separately from the SDK)
    implementation("cloud.shoplive:shoplive-media3:1.10.1.3")     // media3 (default) — minSdk 23
    // implementation("cloud.shoplive:shoplive-exoplayer:2.19.1.20")  // ExoPlayer (legacy) — minSdk 19
}

android {
    defaultConfig { multiDexEnabled = true }
}
Android — you must add the playback engine yourself shoplive-short-form references the shared, networking and player-engine modules as compileOnly. That means if you don't declare them in your app module, the classes are missing at runtime. Use shoplive-media3 by default, or shoplive-exoplayer if your app still uses legacy ExoPlayer.

Where each class comes from

ClassModule (iOS)Package (Android)
ShopLiveCommon · ShopLiveCommonErrorShopliveSDKCommoncloud.shoplive.sdk.common
ShopLiveShortform, collections, previewShopLiveShortformSDKcloud.shoplive.sdk.shorts
Product and shorts response modelsShopliveSDKCommon (ProductData etc.)cloud.shoplive.sdk.network.response
Getting started

Quick Start

Once installation is done, the code below plays your first short.

Get your keys first Your accessKey is issued by Shoplive. If you don't have one, ask your Shoplive contact or look it up in Shoplive Admin. Shorts need no campaign key — you choose what plays with a query (tags, brands, SKUs, group ID).

Full-screen playback — 2 lines

// ① Once at app startup (AppDelegate)
ShopLiveCommon.setAccessKey(accessKey: "YOUR_ACCESS_KEY")

// ② Play — with no query, you get every short
ShopLiveShortform.play(requestData: ShopLiveShortformCollectionData(delegate: nil))
// ① Once at app startup (Application.onCreate)
ShopLiveCommon.setAccessKey("YOUR_ACCESS_KEY")

// ② Play — with no query, you get every short
ShopLiveShortform.play(context)

Those two lines load the shorts, play them in a vertical full-screen feed, and bring along the product cards, likes, comments and share overlays. For details: 1 connect user identity · 2 Mission 1.

Order matters setAccessKey must run before playback. If it hasn't, you get 9000 NOT_INITIALIZED_ACCESS_KEY (error handling). Do it in AppDelegate / Application.onCreate.

Playing with a query

let data = ShopLiveShortformCollectionData(
    tags: ["spring", "outerwear"],
    tagSearchOperator: .AND,        // match every tag (.OR = match any)
    brands: ["shoplive"],
    shuffle: true,
    skus: ["SKU-1001"],
    shortsCollectionId: "COLLECTION_ID",
    isMuted: false,
    delegate: self                  // receive events — Mission 5
)
ShopLiveShortform.play(requestData: data)
val data = ShopLiveShortformCollectionData().apply {
    tags = listOf("spring", "outerwear")
    tagSearchOperator = ShopLiveShortformTagSearchOperator.AND
    brands = listOf("shoplive")
    shuffle = true
    skus = listOf("SKU-1001")
    shortsCollectionId = "COLLECTION_ID"
    isMuted = false
    handler = myHandler             // receive events — Mission 5
}
ShopLiveShortform.play(context, data)

Next stepScreen Terminology Guide · Start with Mission 1 →

Screens & demo

Screen Terminology Guide

This section names every element on screen. The missions, options and events below use the same names. Sign-in required marks elements that only work once the user is identified (Mission 4).

Detail screen (full-screen playback)

A vertical full-screen feed you swipe up and down. Missions 1, 3 and 6 all lead here.

No.ElementShown whenDescription · related API
①Video areaAlwaysVertical video. Fill mode via setResizeMode (CENTER_CROP/FIT/AUTO) — Mission 7
②Creator · brandAlwaysAuthor and brand set in Shoplive Admin
③Title · description · hashtagsAlwaysshortsDetail.title / description / tags
④Product card · product bannerAlwaysA tap calls back into your app — Mission 5 (handleProductItem / getOnClickProductListener)
⑤LikeSign-in requiredHide with isLikeButtonVisible — Mission 7
⑥CommentsSign-in requiredisCommentButtonVisible
⑦BookmarkSign-in requiredisBookmarkVisible
⑧ShareAlwaysA tap asks your app to present the share sheet — Mission 5 (handleShare / onShare)
⑨Mute toggleAlwaysStarts from isMuted; emits VIDEO_MUTED/VIDEO_UNMUTED
⑩Close (back)AlwaysTo close it from your app, call ShopLiveShortform.close()
Annotated captures coming The numbered screenshots used in the SDK v3 guide will land here once they are produced. Numbers ①–⑩ stay as they are when the images arrive.

Collections (embedded in your app)

A collection you place inside an existing screen. What each card shows is switched on and off with options (Mission 7).

TypeShapeWhere it fits
Card (CardType)Grid with a configurable column countCampaign or brand sections
Vertical (Vertical)A collection that continues downwardA whole tab dedicated to shorts
Horizontal (Horizontal)A carousel you swipe sidewaysA strip in the middle of a home feed
Full feed (DetailType / ShortsCollectionView)The detail screen, as a viewApps where a whole tab is shorts
Element in a cardDefaultHow to turn it off
Thumbnail / autoplaying videoPlayingdisablePlayVideos()
TitleShownsetVisibleTitle(false)
DescriptionShownsetVisibleDescription(false)
BrandShownsetVisibleBrand(false)
Product countShownsetVisibleProductCount(false)
View countShownsetVisibleViewCount(false)

Floating preview

A small window floating above another screen. Tapping it expands into the detail screen (Mission 3).

ElementDescription · related API
PositionOne of the four corners — previewPosition / position (default bottomRight)
Size · corner radiusiOS previewScale · previewRadius / Android width · height · radius
MarginsiOS previewEdgeInset / Android marginTop and the other three sides
Close buttonuseCloseButton (shown by default)
Swipe to dismissiOS enableSwipeOut / Android setEnabledSwipeOut
Tap behaviorExpands to the detail screen by default. Your app can intercept it with useCustomAction + clickEventCallBack on iOS, or setUseCustomAction + setOnClickListener on Android

Previous ← Quick StartNext → Try It in the Demo App

Screens & demo

Try It in the Demo App

The demo app shipped with the SDK is organized around the same cases as this guide's missions (mission map). Seeing a behavior in the demo first makes the code much easier to follow.

  1. Default mode — Shoplive demo settings: right after install, every case runs against demo data with no input from you.
  2. My-info mode — enter client settings: enter your accessKey and, optionally, a user JWT in the settings screen to run the same cases against your shorts and your users.
  3. Use the options dialog to change tags, brands, SKUs, shuffle, card type and preview position and watch how the options in Mission 7 show up on screen.
Demo distribution channels (TestFlight / APK) and the demo access key come with the SDK handover.

Previous ← Screen Terminology GuideNext → Mission 1 · Open shorts full screen

Missions · Display

Mission 1 · Open shorts full screen

GoalTap a button and a vertical full-screen feed opens, swipeable up and down
PrerequisitesInstallation done · access key (how to get one)
  1. Confirm setAccessKey runs at app startup (quick start).
  2. In your entry button's action, build the query data and call play.
  3. Optionally attach a delegate or handler to receive product and share events — Mission 5.
@IBAction func openShorts() {
    let data = ShopLiveShortformCollectionData(
        tags: ["spring"],
        tagSearchOperator: .AND,
        shuffle: true,
        delegate: self          // optional: receive events
    )
    ShopLiveShortform.play(requestData: data)
}
fun openShorts() {
    val data = ShopLiveShortformCollectionData().apply {
        tags = listOf("spring")
        tagSearchOperator = ShopLiveShortformTagSearchOperator.AND
        shuffle = true
        handler = myHandler     // optional: receive events
    }
    ShopLiveShortform.play(context, data)
}
Starting from one specific short Pass shortsId and that short opens first, with the rest following behind it. This is what a push notification or deep link to a specific short should do.
Controlling the transition yourself (Android) ShopLiveShortform.intent(context, data) returns an Intent. Call startActivity yourself to control the transition and flags.

Closing

ShopLiveShortform.close()      // when your app must close it (sign-out, deep link, …)
ShopLiveShortform.close()      // when your app must close it (sign-out, deep link, …)

Completion Checklist

  • The video starts within a second or two of the tap
  • Swiping up and down moves to the next and previous short
  • Shorts that have products show a product card at the bottom (glossary ④)
  • If a wrong query returns nothing, the empty state appears — check you didn't over-narrow it

Related: Events · UI optionsNext → Mission 2 · Embed a collection

Missions · Display

Mission 2 · Embed a collection in your screen

GoalPut a collection inside your home or campaign screen, tapping through to full screen
PrerequisitesMission 1 done (you understand the query options)

Lists come in three shapes — card (grid), vertical and horizontal (carousel). All three take the same query and the same display options. You only pick the shape.

Step 1 · Create the view and attach it

// iOS — build it, then take the view out with getView()
private var builder: ShopLiveShortform.CardTypeViewBuilder?

override func viewDidLoad() {
    super.viewDidLoad()

    let builder = ShopLiveShortform.CardTypeViewBuilder()
    let listView = builder.build(
        cardViewType: .type1,
        listViewDelegate: self,
        shortsCollectionDelegate: self,   // product and share events — Mission 5
        enableSnap: false,
        enablePlayVideo: true,
        playOnlyOnWifi: false,
        cellSpacing: 8,
        cellRadius: 16,
        viewCountVisibility: true
    ).getView()

    listView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(listView)
    // … Auto Layout constraints …

    builder.setHashTags(tags: ["spring"], tagSearchOperator: .AND)
    builder.submit()                       // ← required once the query is set
    self.builder = builder
}
// Android — place the view in XML, configure it in code, then submit()
// res/layout/fragment_home.xml
// <cloud.shoplive.sdk.shorts.ShopLiveShortformCardTypeView
//     android:id="@+id/shortsCardTypeView"
//     android:layout_width="match_parent"
//     android:layout_height="0dp" />

with(binding.shortsCardTypeView) {
    setSpanCount(2)                                        // grid columns
    setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
    handler = myHandler                                    // product and share events — Mission 5
    setHashTags(listOf("spring"), ShopLiveShortformTagSearchOperator.AND)
    submit()                                               // ← required once the query is set
}
listViewDelegate has two required methods (iOS) ShopLiveShortformListViewDelegate must inherit NSObject, and both onListViewError(error:) and onShortsSettingsInitialized() are non-optional. Leave either one out and it will not compile. onShortsSettingsInitialized() is called only for card views built with CardTypeViewBuilder.
extension MyViewController: ShopLiveShortformListViewDelegate {
    func onListViewError(error: Error) {
        // The list query failed — show a retry, for example
    }
    func onShortsSettingsInitialized() {
        // Server settings received — it is safe to re-apply options from here
    }
}
Forget submit() and the collection stays empty Query options (setHashTags, setBrands, setSkus, setShortsCollectionId) do nothing on their own. The server query happens when you call submit(). The same applies when you change the query and reload (on iOS, reloadItems() also works).

Step 2 · Pick the shape

// Vertical and horizontal come from ListViewBuilder
let builder = ShopLiveShortform.ListViewBuilder()
let listView = builder.build(
    cardViewType: .type1,
    listViewType: .horizontal,        // .vertical | .horizontal
    playableType: .CENTER,            // which cell plays in a horizontal list
    listViewDelegate: self,
    shortsCollectionDelegate: self
).getView()
builder.submit()
// Vertical — ShopLiveShortformVerticalTypeView
with(binding.shortsVerticalTypeView) {
    setSpanCount(2)
    setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
    submit()
}

// Horizontal — ShopLiveShortformHorizontalTypeView
with(binding.shortsHorizontalTypeView) {
    setViewType(ShopLiveShortform.CardViewType.CARD_TYPE1)
    setPlayableType(ShopLiveShortform.PlayableType.CENTER)
    enableSnap()
    submit()                                     // ← horizontal lists need submit() too
}
Horizontal collections are fed data you already have (Android) The horizontal submit(collectionData, response) takes a response you already fetched. Query it first with ShopLiveShortform.get(context, collectionData, listener) and pass it in — handy when one query fills several carousels on a home screen.

Step 3 · Stop playback when the screen goes away

Turning autoplay off when the screen isn't visible saves battery and data.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    builder?.enablePlayVideos()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    builder?.disablePlayVideos()
}

override func viewWillTransition(to size: CGSize, with c: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: c)
    builder?.notifyViewRotated()          // recalculate layout on rotation
}
override fun onResume() {
    super.onResume()
    binding.shortsCardTypeView.enablePlayVideos()
}

override fun onPause() {
    super.onPause()
    binding.shortsCardTypeView.disablePlayVideos()
}

Step 4 (optional) · Make a whole screen the shorts feed

Instead of presenting the full-screen feed, you can turn one of your tabs into it.

let feed = ShopLiveShortsCollectionView(
    requestData: ShopLiveShortformCollectionData(tags: ["spring"], delegate: self)
)
view.addSubview(feed)

feed.resultHandler = { result in
    if case .didScrollToShortsId(let shortsId) = result {
        print("current short: \(shortsId ?? "")")
    }
}

feed.action(.play)          // .pause · .setMuted(true) · .setScrollEnabled(false) …
// res/layout — <cloud.shoplive.sdk.shorts.ShopLiveShortformDetailTypeView … />
with(binding.shortsDetailTypeView) {
    setMuted(false)
    setUIComponentPadding(bottom = bottomNavHeight)   // keep clear of the bottom tab bar
    setHashTags(listOf("spring"), ShopLiveShortformTagSearchOperator.AND)
    submit()                                          // ← always call once the query is set
}

// Forward the system back gesture to the feed
override fun onBackPressed() {
    binding.shortsDetailTypeView.onBackPressed()
}

Completion Checklist

  • The collection fills with thumbnails and visible cells start playing
  • Tapping a card opens the full-screen detail
  • Moving to another tab stops playback (disablePlayVideos)
  • Changing the query and calling submit() refills the collection

Related: Hiding card elementsNext → Mission 3 · Floating preview

Missions · Display

Mission 3 · Show a floating preview

GoalFloat a small window with related shorts over a product page, expanding to full screen on tap
PrerequisitesMission 1 done

Previews connect products to shorts. Someone on a product page sees a small window with the short that features that product, and if they're interested it expands to full screen.

  1. When entering the product screen, pick related shorts with productId or skus.
  2. Set the display options — position, size, close button.
  3. Dismiss the preview when the user leaves the screen.
let preview = ShopLiveShortformPreviewData(
    productId: "PRODUCT_ID",
    isMuted: true,
    previewPosition: .bottomRight,
    previewRadius: 12,
    previewEdgeInset: .init(top: 0, left: 0, bottom: 24, right: 16),
    useCloseButton: true,
    enableSwipeOut: true,
    maxCount: 10,
    delegate: self
)
ShopLiveShortform.showPreview(requestData: preview)

// Dismissing
ShopLiveShortform.close()
ShopLiveShortform.hidePreview()          // dismiss first so you never stack two
ShopLiveShortform.showPreview(
    activity,
    ShopLiveShortformPreviewData().apply {
        productId = "PRODUCT_ID"
        isMuted = true
        setPosition(ShopLivePreviewPositionConfig.BOTTOM_RIGHT)
        setRadius(12f)
        setMarginBottom(24)
        setMarginRight(16)
        setUseCloseButton(true)
        setIsAutoClosed(true)
        maxCount = 10
        handler = myHandler
    }
)

// Dismissing
ShopLiveShortform.hidePreview()
// Is one on screen right now?
if (ShopLiveShortform.isShowingPreview()) { /* … */ }
Always dismiss it when leaving the screen The preview floats above the app, not inside your screen. If you don't dismiss it when the user leaves the product page, it stays there on top of something else. Use hidePreview() on Android and close() on iOS.
Intercepting the tap (iOS) Pass useCustomAction: true together with clickEventCallBack and a tap sends the user wherever your app wants instead of expanding to full screen.

Completion Checklist

  • Entering the product page floats a small window in the corner you chose
  • Tapping it expands to the full-screen detail
  • The close button (or a swipe) dismisses it
  • Navigating elsewhere leaves no preview behind

Related: Product tap eventsNext → Mission 4 · Connect user identity

Missions · Integration

Mission 4 · Connect user identity

GoalLet signed-in users like, comment and bookmark, and have it still be there next time
PrerequisitesAccess key set · (for secure auth) backend JWT issuance ready (Step 4)
setAuthToken and setUser pass user information — they do not authenticate the user Sign-in (ID · password, social login and so on) stays entirely in your app. setAuthToken and setUser only tell Shoplive who the current viewer is so that likes, comments, bookmarks and viewing history are attributed to that member. Shoplive does not check the user's credentials. With a JWT (setAuthToken), Shoplive can verify that the information was issued by your server and not altered (signature check); with a profile (setUser), the information is passed as is, without that check.

Step 1 · Do you need it? — how to decide

SituationNeeded?
Users only watch (playback, scrolling, product taps)No — works signed out
Users like, comment or bookmarkYes — without identity nothing is recorded
You need per-user history or personalizationYes

Step 2 · Choose by sign-in state — recommended setup

UserRecommendedWhy
Signed outCall nothingThe server issues the guest identifier. To turn a signed-in user back into a guest, reset the user info with the sign-out calls below
Signed in (recommended)setAuthToken — a JWT signed by your serverIt can't be forged, so you can trust that likes, comments, bookmarks and viewing history belong to that member
Signed in (when backend work is hard)setUser — pass the profile (userId, name, and so on)Unsigned, client-supplied values that can be forged. It's enough for identification and stats, but don't use it for authorization
Log out · reset user infosetAuthToken(nil/null) + setUser(nil/null) (iOS and Android)Clears the user info you set (JWT and profile) so the previous user's activity doesn't bleed through. The API is named setAuthToken, but the value you pass is user info (a JWT)
Recommended flow App launch (signed out) → do nothing → signed in: right before each visit to a shorts screen, get a newly issued JWT from your backend and call setAuthToken → on sign-out, reset the user info.
// ① Signed in · secure auth (recommended) — a JWT signed by your backend (Step 4)
ShopLiveCommon.setAuthToken(authToken: myJwtFromServer)

// ② Signed in · simple — pass the profile directly (unsigned · not for authorization)
ShopLiveCommon.setUser(user: ShopLiveCommonUser(
    userId: hashedUserId,          // a hash, not the raw member ID (Step 3)
    userName: "Jane Doe",
    age: 29,
    gender: .female,
    userScore: 3
))

// ③ Signed out — no call needed

// Sign-out — reset the user info (clears the JWT and profile)
ShopLiveCommon.setAuthToken(authToken: nil)
ShopLiveCommon.setUser(user: nil)

// Current state
let isLoggedIn = ShopLiveCommon.isLoggedIn()
// ① Signed in · secure auth (recommended) — a JWT signed by your backend (Step 4)
ShopLiveCommon.setAuthToken(myJwtFromServer)

// ② Signed in · simple — pass the profile directly (unsigned · not for authorization)
ShopLiveCommon.setUser(
    ShopLiveCommonUser(userId = hashedUserId).apply {   // a hash, not the raw member ID (Step 3)
        setUserName("Jane Doe")
        setAge(29)
        setGender(ShopLiveCommonUserGender.FEMALE)
    }
)

// ③ Signed out — no call needed

// Sign-out — reset the user info (clears the JWT and profile)
ShopLiveCommon.setAuthToken(null)
ShopLiveCommon.setUser(null)
clearAuth() clears a different scope on each platform On Android it clears only the user info (user, JWT, userId). On iOS it resets the whole common state, including your accessKey, guest identifier and UTM, so if you use it, set the accessKey again before the next use. To clear only the user info, pass nil (null) to setAuthToken and setUser as shown above.

Step 3 · ID hashing — don't expose raw member IDs

Never pass the raw member ID userId is stored on Shoplive servers and in Shoplive Admin, and can be visible to other users. Hash your internal member ID before passing it, and keep a mapping table so your app can go back the other way.

Use a value that your app and backend hash the same way as the userId — in the JWT and in the profile alike. The key is using a consistent salt and algorithm so the same user always produces the same value (to keep likes and bookmarks, and for statistics and return-visitor identification).

View hashing code samples (Swift · Kotlin)
import CryptoKit

func hashUserId(_ raw: String) -> String {
    let salted = raw + "APP_SALT"
    let digest = SHA256.hash(data: Data(salted.utf8))
    return digest.map { String(format: "%02x", $0) }.joined()
}
import java.security.MessageDigest

fun hashUserId(raw: String): String {
    val salted = raw + "APP_SALT"
    return MessageDigest.getInstance("SHA-256")
        .digest(salted.toByteArray())
        .joinToString("") { "%02x".format(it) }
}
Security note Hashing only obscures the raw value — it is not authentication. If you need to verify permissions, always use a server-signed JWT (setAuthToken). We recommend agreeing on the salt policy with the Shoplive team.

Step 4 · Create the JWT for secure auth (your backend)

The JWT you pass to setAuthToken is signed by your backend with the Secret Key that Shoplive issues. The app only fetches the JWT from your backend and passes it to setAuthToken. It is the same JWT as the Live (Player) SDK.

  1. Get the Secret Key from your Shoplive contact. It's a base64-encoded string
  2. Build the payload below from the signed-in user and sign it with HS256
  3. Issue a new JWT every time the app asks (right before each visit to a shorts screen). Don't store a JWT in the app and reuse it
Generate the JWT fresh each time Don't cache a JWT on the device or reuse an old one. User details (name, grade, type) may have changed since the last issue, and a token that leaks stays usable until its exp. Have the app request a new JWT from your backend whenever it calls setAuthToken.
claimRequiredDescriptionExample
expRequiredExpiration time (Unix seconds)1516239022
userIdRequiredUnique value for the signed-in user (up to 255 bytes). A hashed value works too (Step 3)sample_user_id
iatIssued-at time (Unix seconds)1516195822
nameName shown in chat. Banned words are masked with asterisks (*)LiveFan
genderGender — m · f · empty (unspecified)f
ageAge. Stats group it in 5-year bands27
userScoreUser grade (-100 to 100). The basis for grade-based entry and prize-draw limits10
userTypeUser type. Lets only certain types into broadcasts with an entry restriction (Entry Type)VIP
profileChat profile image URL (64×64px recommended)https://…/profile.png
customArbitrary data (up to 1 KB){"grade":"vip"}

Signing examples. Both produce the same token — just note that the Secret Key is a base64 string.

// Node.js — npm i jsonwebtoken
const jwt = require("jsonwebtoken");

const secret = Buffer.from(process.env.SHOPLIVE_SECRET_KEY, "base64");  // base64-decode it into bytes
const token = jwt.sign(
  { userId: hashedUserId, name: "LiveFan", gender: "f", age: 27, userScore: 10 },
  secret,
  { algorithm: "HS256", expiresIn: "12h" }   // fills in exp and iat automatically
);
// Java — io.jsonwebtoken (jjwt)
String jwt = Jwts.builder()
    .signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey)   // pass the base64 string as is
    .setExpiration(new Date(System.currentTimeMillis() + 12 * 60 * 60 * 1000L))
    .setIssuedAt(new Date())
    .claim("userId", hashedUserId)
    .claim("name", "LiveFan")
    .claim("gender", "f")
    .claim("age", 27)
    .claim("userScore", 10)
    .compact();
Keep the Secret Key on your backend If it ships in app code or the bundle, anyone can extract it and forge tokens for other users. Create JWTs on your backend only; the app just passes the token it receives to setAuthToken. A profile (setUser) is unsigned, so you can't use it for authorization.

Step 5 · When to call it

  • Right after sign-in — before the user reaches a shorts screen
  • Each time the user enters a shorts screen — call setAuthToken again with a newly issued JWT from your backend (don't reuse a stored token)
  • On sign-out — reset the user info so the previous user's activity doesn't bleed through

Completion Checklist

  • After signing in, a like survives an app restart
  • After signing in, writing a comment isn't blocked
  • After sign-out → resetting the user info, likes and bookmarks reset when you come back (you watch as a guest)
Same user info as the Live SDK If you already use the Live (Player) SDK, the same JWT and the same user apply here. An app using both SDKs configures authentication once, in one place.

Previous ← Mission 3Next → Mission 5 · Events, products, sharing

Missions · Integration

Mission 5 · Events, products, sharing

GoalSend product taps to your own detail page, and sharing through your own share sheet
PrerequisitesMission 1 or Mission 2 done

The SDK draws the screen and leaves the decisions to you. Where a product tap goes and what link sharing produces are both your app's calls. The delegate (iOS) / handler (Android) is how they reach you.

Delegate surface — three groups

WhatiOSAndroid
Product taphandleProductItem(shortsId:shortsSrn:product:)getOnClickProductListener()
Product banner taphandleProductBanner(shortsId:shortsSrn:scheme:)getOnClickBannerListener()
Share taphandleShare(shareMetadata:) · handleShare(shareUrl:)onShare(context, data) · onShare(context, url)
General eventsonEvent(messenger:command:payload:)onEvent(context, messenger, command, payload)
ErrorsonError(error:)onError(context, error)
Appear / disappearonDidAppear() · onDidDisAppear()onCreate() · onDestroy()
Short changedonShortsAttached/Detached(data:)onShortsAttached/Detached(data)

Step 1 · Wire it up

// Put the delegate on the data object (same for playback, preview and lists)
let data = ShopLiveShortformCollectionData(delegate: self)
ShopLiveShortform.play(requestData: data)

extension MyViewController: ShopLiveShortformReceiveHandlerDelegate {
    // Implement only what you need — every method is optional
}
// Declare the handler first
val myHandler = object : ShopLiveShortformHandler() {
    // Override only what you need
}

// ① Per data object / per view — this is the normal way
val data = ShopLiveShortformCollectionData().apply { handler = myHandler }
binding.shortsCardTypeView.handler = myHandler

// ② Global — only read on the hybrid (WebView) bridge path
ShopLiveShortform.setHandler(myHandler)

Step 2 · Product tap → your detail page

func handleProductItem(shortsId: String, shortsSrn: String, product: ProductData) {
    // Route on whichever key your app uses: url · sku · productId · customerProductId
    guard let urlStr = product.url, let url = URL(string: urlStr) else { return }
    navigationController?.pushViewController(ProductDetailViewController(url: url), animated: true)
}

func handleProductBanner(shortsId: String, shortsSrn: String, scheme: String) {
    // Banners hand you the scheme or URL configured in the console, as-is
    openDeepLink(scheme)
}
override fun getOnClickProductListener() = ShopLiveShortformProductListener { context, data, product ->
    // Route on whichever key your app uses: product.url · sku · productId
    product.url?.takeIf { it.startsWith("http") }?.let { url ->
        ProductDetailActivity.start(context, url)
    }
}

override fun getOnClickBannerListener() = ShopLiveShortformUrlListener { context, data, url ->
    openDeepLink(context, url)
}
Prefer customerProductId productId is Shoplive's internal identifier. To reach your own product page, route on sku or url.
On iOS the callback also carries the merchant product ID (customerProductId), but Android's product callback model has no such field.

Step 3 · Share tap → your share sheet

On a share tap the SDK calls back into your app. If Shoplive Admin or the web layer supplies a share URL you get handleShare(shareUrl:); otherwise you get handleShare(shareMetadata:). Your app builds its own deep link or dynamic link and presents the share sheet.

iOS — leave handleShare(shareUrl:) unimplemented and the SDK presents its own sheet If you implement only the shareMetadata overload, shares that arrive through the URL path make the SDK present the default iOS share sheet instead. To keep sharing entirely yours, implement both overloads.
func handleShare(shareMetadata: ShopLiveShareMetaData) {
    // title · descriptions · thumbnail · shortsId · shortsSrn
    let link = makeMyDeepLink(shortsId: shareMetadata.shortsId)
    let vc = UIActivityViewController(activityItems: [shareMetadata.title ?? "", link],
                                      applicationActivities: nil)
    // Shorts live in their own window — present relative to that window
    ShopLiveShortform.getCurrentKeyWindow()?.rootViewController?.present(vc, animated: true)
}
override fun onShare(context: Context, data: ShopLiveShortformShareData) {
    // shortsId · srn · title · description · thumbnail
    val link = makeMyDeepLink(data.shortsId)
    val send = Intent(Intent.ACTION_SEND).apply {
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, "${data.title}\n$link")
    }
    context.startActivity(Intent.createChooser(send, null))
}
iOS — mind which window you present from The full-screen feed lives in the SDK's own UIWindow. Presenting from your app's root view controller puts the share sheet behind the shorts. Always present relative to the window returned by ShopLiveShortform.getCurrentKeyWindow().

Step 4 · Receiving general events

func onEvent(messenger: ShopLiveShortformMessenger?, command: String, payload: String?) {
    switch command {
    case "VIDEO_MUTED":              isMuted = true
    case "VIDEO_UNMUTED":            isMuted = false
    case "DETAIL_ON_PLAYER_SHOWN":   analytics.log("shorts_detail_open")
    case "COLLECTION_CLICK_ITEM":    analytics.log("shorts_card_click")
    default:                         break            // ignore commands you don't know
    }
}
override fun onEvent(
    context: Context,
    messenger: ShopLiveShortformMessageListener?,
    command: String,
    payload: Map<String, Any?>
) {
    when (command) {
        "VIDEO_MUTED"   -> isMuted = true
        "VIDEO_UNMUTED" -> isMuted = false
        else -> Unit                                  // ignore commands you don't know
    }
}
Commands get added — always keep a default New event commands appear in SDK updates. Handle the ones you know and ignore the rest, and upgrading the SDK will never break your app. The list is in Types & States.

Completion Checklist

  • Tapping a product card opens your app's product detail
  • Tapping share opens a share sheet with your link, on top of the shorts screen
  • Toggling mute logs VIDEO_MUTED/VIDEO_UNMUTED
  • Failures surface with a code/message in onError (error handling)

Previous ← Mission 4Next → Mission 6 · Play your own list

Missions · Integration

Mission 6 · Play your own list

GoalPlay exactly the IDs and order your server decides, appending the next page at the end
PrerequisitesMission 1 done · an API of yours that returns shorts IDs

The query options in Mission 1 let Shoplive decide the list. When your server has to decide the order — a recommendation feed, for example — pass the shorts IDs directly.

  1. Fetch the first page of shorts IDs from your API.
  2. Pass the IDs and a starting point (currentId) to play.
  3. When the SDK reaches the end it asks for the next page — your app fetches and fills it.
// 1 and 2) Play the first page
let ids = firstPage.map { ShopLiveShortformIdData(shortsId: $0.id, payload: nil) }
ShopLiveShortform.play(
    shortformIdsData: ShopLiveShortformIdsData(ids: ids, currentId: ids.first?.shortsId),
    dataSourceDelegate: self,
    shortsCollectionDelegate: self
)

// ③ Pagination — downward and upward, separately
extension MyFeed: ShortsCollectionViewDataSourcRequestDelegate {
    func onShortformListDownwardPagination(
        completion: @escaping ((ShopLiveShortformIdsMoreData?, Error?)) -> ()
    ) {
        myAPI.next { page, error in
            guard let page = page else { completion((nil, error)); return }
            let more = ShopLiveShortformIdsMoreData(
                ids: page.items.map { ShopLiveShortformIdData(shortsId: $0.id, payload: nil) },
                hasMore: page.hasMore
            )
            completion((more, nil))
        }
    }

    func onShortformListUpwardPagingation(
        completion: @escaping ((ShopLiveShortformIdsMoreData?, Error?)) -> ()
    ) {
        completion((nil, nil))          // nil when there is nothing above
    }
}
// 1, 2 and 3) Playback plus pagination (coroutine flavor)
val idsData = ShopLiveShortformIdsData().apply {
    ids = firstPage.map { ShopLiveShortformIdData(it.id) }
    currentId = firstPage.firstOrNull()?.id
    handler = myHandler
}

ShopLiveShortform.play(
    context,
    idsData,
    moreSuspendListener = {                       // load more, downward
        val page = myApi.next()
        ShopLiveShortformIdsMoreData().apply {
            ids = page.items.map { ShopLiveShortformIdData(it.id) }
            hasMore = page.hasMore
        }
    },
    previousSuspendListener = { null }            // null when there is nothing above
)

// Not using coroutines? There is a Future variant too
// ShopLiveShortform.play(context, idsData, moreFutureListener, previousFutureListener)
Always send hasMore = false at the end If the last page doesn't set hasMore to false, the SDK keeps asking for more pages. Signal the end with an empty array plus hasMore=false.
You can carry your own data along Put a recommendation reason, a ranking, anything of yours into ShopLiveShortformIdData.payload and you get it back in the event callbacks for logging.

Completion Checklist

  • Playback follows exactly the order your server sent
  • The short named by currentId is the first screen
  • Scrolling to the end appends the next page
  • After the last page, requests stop (hasMore=false)

Previous ← Mission 5Next → Mission 7 · Customize the UI

Missions · Integration

Mission 7 · Customize the UI

GoalHide features your service doesn't have (comments, say), and match card content and video fill
PrerequisitesMission 1 and Mission 2 done

Option ① · Hide detail-screen buttons

Like, comment, bookmark and share can each be turned off. Hiding what your service doesn't offer avoids confusion.

ShopLiveShortform.setVisibileDetailViews(options: ShopLiveShortformVisibleDetailData(
    isBookMarkVisible: false,
    isShareButtonVisible: true,
    isCommentButtonVisible: false,
    isLikeButtonVisible: true
))
ShopLiveShortform.setVisibleDetailTypeViews(
    ShopLiveShortformVisibleDetailTypeData(
        isBookmarkVisible = false,
        isShareButtonVisible = true,
        isCommentButtonVisible = false,
        isLikeButtonVisible = true
    )
)
Set it before playback Visibility is a global setting. Apply it before calling play or showPreview for it to take effect on that screen.

Option ② · Video fill mode

ShopLiveShortform.setResizeMode(mode: .CENTER_CROP)   // .FIT | .AUTO
ShopLiveShortform.setResizeMode(ShopLiveResizeMode.CENTER_CROP)   // FIT | AUTO
ValueBehaviorWhere it fits
CENTER_CROPFills the screen, cropping what doesn't fitServices with vertical video only
FITFits the whole frame, letterboxing as neededA mix of landscape and square video
AUTOThe SDK picks based on the video's ratioDefault · mixed aspect ratios

Option ③ · What a collection card shows

builder.setVisibleTitle(isVisisble: true)
builder.setVisibleDescription(isVisible: false)
builder.setVisibleBrand(isVisible: true)
builder.setVisibleProductCount(isVisible: true)
builder.setVisibleViewCount(isVisible: false)
builder.setCellSpacing(spacing: 8)
builder.setCellCornerRadius(radius: 16)
builder.setCellBackgroundColor(color: .white)
builder.setCardViewType(type: .type2)
builder.reloadItems()                        // apply the changes
with(binding.shortsCardTypeView) {
    setVisibleTitle(true)
    setVisibleDescription(false)
    setVisibleBrand(true)
    setVisibleProductCount(true)
    setVisibleViewCount(false)
    setRadius(16f.toPx(context))
    setCardBackgroundColor(Color.WHITE)
    setViewType(ShopLiveShortform.CardViewType.CARD_TYPE2)
    submit()                                 // apply the changes
}

Option ④ · Platform-specific settings

OptionPlatformDescription
setSystemBarColor(data)AndroidMatch the detail screen's status and navigation bars to your theme
setSoftInputMode(mode)AndroidKeyboard behavior while commenting (ADJUST_RESIZE by default)
setMaxCount(n)Android (collections)Cap how many items a collection shows
setEnableResumeOnForeGround(enable:)iOSWhether playback resumes when the app returns to the foreground
avAudioSessionCategoryOptionsiOS (builder)How audio mixes with other apps
setPlayOnlyWifi(true)BothAutoplay collections on Wi-Fi only — considerate of data-conscious users

Completion Checklist

  • Buttons you turned off aren't on the detail screen
  • Video fills the way you intended (cropped or fully visible)
  • Cards show only the elements you kept
  • On Android the status bar color matches your theme

Previous ← Mission 6Next → Appendix · Extra features

Appendix

Extra Features

Things outside the missions, for when you need them.

Fetching collection data only (Android)

Use this to get data without showing a screen — to draw it yourself, or to fill several horizontal carousels at once.

ShopLiveShortform.get(context, collectionData, object : ShopLiveShortformCollectionListener {
    override fun onData(data: ShopLiveShortformCollectionResponse) {
        binding.shortsHorizontalTypeView.submit(collectionData, data)
    }
    override fun onError(e: Exception) { /* … */ }
})
// iOS has no equivalent public API.
// If you only need the data, query it from your server with the Shoplive Open API
// and feed the IDs in through Mission 6 (play your own list).
get() needs a LifecycleOwner It runs on lifecycleScope internally, so the context has to be an Activity or Fragment. Anything else and nothing happens, silently. Don't pass applicationContext.

Video cache control (iOS)

Shorts cache the videos they play on disk. In a long-lived app that adds up, so if your settings screen has a "clear cache" action, clear this along with it.

// Clear what is cached right now
AVAssetDownloadManager.shared.deleteCaches()

// Or have it cleared automatically on app termination (call once at startup)
AVAssetDownloadManager.shared.enableDeleteCacheOnTerminate(isEnabled: true)
// Android exposes no equivalent public cache-control API.
// It follows the playback engine's cache policy (media3 / ExoPlayer).

Attribution (referrer and UTM)

Record which screen sent the user into shorts so you can analyze conversion.

iOS — creating a data object clears the global referrer The initializers of ShopLiveShortformCollectionData, ShopLiveShortformPreviewData and ShopLiveShortformIdsData overwrite the global value even when you omit referrer — with nil. A referrer set earlier through setReferrer disappears the moment you build one of these, so always pass referrer: in the initializer on screens that use a data object.
ShopLiveShortform.setReferrer(referrer: "home_banner")      // global
// Or at construction: ShopLiveShortformCollectionData(referrer: "home_banner", …)

ShopLiveCommon.setUtmSource(utmSource: "naver")
ShopLiveCommon.setUtmMedium(utmMedium: "cpc")
ShopLiveCommon.setUtmCampaign(utmCampaign: "spring_sale")
ShopLiveCommon.setUtmContent(utmContent: "banner_a")
val data = ShopLiveShortformCollectionData().apply { referrer = "home_banner" }
// List views: binding.shortsCardTypeView.setReferrer("home_tab")

ShopLiveCommon.setUtmSource("naver")
ShopLiveCommon.setUtmMedium("cpc")
ShopLiveCommon.setUtmCampaign("spring_sale")
ShopLiveCommon.setUtmContent("banner_a")

Hybrid (WebView) integration

For showing shorts inside a web view in your app: the native SDK receives the shorts calls the page makes.

// Connect right after creating the web view
ShopLiveShortform.BridgeInterface.connect(myWebView)

// To show an app screen (e.g. product detail) on top of the shorts full screen
ShopLiveShortform.BridgeInterface.present(viewController: productDetailVC)

// When leaving the screen
ShopLiveShortform.BridgeInterface.disconnect()
// Right after creating the web view, register the bridge as a JavaScript interface
val bridge = ShopLiveShortformBridgeInterface(myWebView, null)
myWebView.addJavascriptInterface(bridge, "ShopLiveBridgeInterface")
bridge.observe(activity)          // receive messages for the ComponentActivity's lifetime

// When leaving the screen
bridge.removeObserver()
present(viewController:) is not for connecting (iOS) Connect with connect · disconnect. present(viewController:) shows the screen you pass on top of the shorts full screen (overFullScreen).
Reference

Data Field Dictionary

Query options — choosing which shorts to show

These mean the same thing in playback, collections and previews.

FieldTypeMeaning
shortsIdStringOpen this short first (used for deep links)
shortsCollectionIdStringQuery by a collection created in Shoplive Admin
tagsString[]Filter by hashtag
tagSearchOperatorAND · ORAND = match every tag · OR = match any (types)
brandsString[]Filter by brand identifier
skusString[]Only shorts linked to these SKUs
productIdStringShorts related to a product (mostly for previews)
shuffleBoolRandomize the order so it differs each time
isMutedBoolStart muted
referrerStringRecord the entry point (appendix)
Options combine with AND Passing tags and brands together returns only shorts that satisfy both. If you get nothing back, remove conditions one at a time.

ShopLiveShortformCollectionData — playback and collection queries

AspectiOSAndroid
ConstructionShopLiveShortformCollectionData(…, delegate:) — init argumentsShopLiveShortformCollectionData().apply { … } — properties
Receiving eventsdelegatehandler
Query optionsSame as the query options table above

ShopLiveShortformPreviewData — floating preview

The query options plus display options.

OptioniOSAndroidDefault
PositionpreviewPositionsetPosition(…)bottomRight
SizepreviewScalesetWidth · setHeightiOS derived from screen width · Android long side 180dp
Corner radiuspreviewRadiussetRadius10
Outer marginpreviewEdgeInsetsetMargin{Top,Bottom,Left,Right}16
Close buttonuseCloseButtonsetUseCloseButtontrue
Swipe to dismissenableSwipeOutsetEnabledSwipeOuttrue
Max itemsmaxCountmaxCountUnlimited
Intercept tapuseCustomAction + clickEventCallBacksetUseCustomAction + setOnClickListenerfalse
Haptic on tap—setCanVibrated—
Volume keysisEnabledVolumeKeyisEnabledVolumeKeyfalse
Auto dismiss—setIsAutoClosed—
Transition animation—setCanTransition—
Android size, margin and radius values are in px Pass px, not dp, to setWidth · setHeight · setMargin… · setRadius. The defaults (long side 180 · margin 16 · radius 10) are in dp and are converted to px for the device density.

ShopLiveShortformIdsData — playing your own list

FieldTypeMeaning
idsShopLiveShortformIdData[]Shorts IDs to play (array order = playback order)
currentIdStringWhich short opens first
isMutedBoolStart muted (Android)
referrerStringEntry point
ShopLiveShortformIdData / IdsMoreDataMeaning
IdData.shortsIdShorts ID
IdData.payloadArbitrary data your app carries along (reason, ranking, …)
IdsMoreData.idsNext page of IDs to append
IdsMoreData.hasMorefalse stops the SDK from asking for more

Collection options

OptioniOS (builder)Android (view)
Card shapesetCardViewType(type:)setViewType(type)
Column count—setSpanCount(n)
Snap scrollingenableSnap() / disableSnap()enableSnap() / disableSnap()
AutoplayenablePlayVideos() / disable…enablePlayVideos() / disable…
Wi-Fi onlysetPlayOnlyWifi(isEnabled:)setPlayOnlyWifi(isEnabled)
Cell spacingsetCellSpacing(spacing:)Layout / item decoration
Corner radiussetCellCornerRadius(radius:)setRadius(radius)
Background colorsetCellBackgroundColor(color:)setCardBackgroundColor(colorInt)
Visible elementssetVisibleTitle/Description/Brand/ProductCount/ViewCountSame
What plays (horizontal)setPlayableType(type:)setPlayableType(type)
Max items—setMaxCount(n)
Scroll to topscrollToTop()scrollToTop(isSmooth)
Apply the querysubmit() · reloadItems()submit()

Detail-screen visibility

FieldDefaultDescription
isLikeButtonVisibletrueLike — needs identity
isCommentButtonVisibletrueComments — needs identity
isBookMarkVisible (Android: isBookmarkVisible)trueBookmark — needs identity
isShareButtonVisibletrueShare — handled by your app

User identity (ShopLiveCommonUser)

FieldRequiredDescription
userId✔Member identifier — pass a hash, not the raw ID (Mission 4)
userNameDisplay name
ageAge
genderiOS .male/.female/.netural · Android MALE/FEMALE/NEUTRAL
userScoreTier or score, used for personalization
customAny extra values your app wants to carry

ProductData — what a product callback gives you

FieldDescription
productIdShoplive's internal product ID
customerProductIdYour product ID — iOS only (absent from the Android callback model)
skuStock keeping unit
urlProduct URL registered in Shoplive Admin
name · brand · descriptionsDisplay information
imageUrlProduct image
currency · originalPrice · discountPrice · discountRatePricing
showPriceWhether to show the price (console setting)
stockStatusStock state

Share metadata

FieldDescription
shortsId · shortsSrn (Android srn)Shorts identifiers — what to put in your deep link
title · descriptionTitle and description for the share text
thumbnailThumbnail URL for the OG image
Reference

Types & States

Card and collection types

TypeiOSAndroid
Card shapeCardViewType.type0 / type1 / type2CardViewType.CARD_TYPE0 / 1 / 2
Collection directionListViewType.vertical / horizontalSeparate view classes (Vertical / Horizontal)

PlayableType — what plays in a collection

ValueBehaviorWhere it fits
FIRSTOnly the first cell plays (default)Saving battery and data
CENTERThe centered cell playsHorizontal carousels
ALLEvery visible cell playsMaximum impact — watch performance

PreviewPosition

iOSAndroid
.topLeft · .topRight · .bottomLeft · .bottomRight · .default TOP_LEFT · TOP_RIGHT · BOTTOM_LEFT · BOTTOM_RIGHT
On iOS, .default is the same as bottomRight.

ShopLiveResizeMode

ValueDescription
CENTER_CROPFill the screen, cropping the overflow
FITFit the whole frame
AUTOChosen from the video's ratio (default)

TagSearchOperator

ValueMeaning
ANDShorts carrying all of the given tags
ORShorts carrying any of them

Event commands

What arrives in onEvent (Mission 5). This list can grow, so handle what you know and ignore the rest.

CommandWhen
VIDEO_MUTED / VIDEO_UNMUTEDMute toggled
DETAIL_ON_PLAYER_SHOWN (iOS)
DETAIL_ON_PLAYER_SHOW (Android)
Entered the full-screen detail — the string differs by platform (no trailing N on Android)
DETAIL_EMPTYThe query returned nothing — too narrow, or no matching shorts in Shoplive Admin
DETAIL_ON_PLAYER_DISMISSLeft the detail
COLLECTION_SHOWA collection became visible
COLLECTION_CLICK_ITEMA collection card was tapped
PREVIEW_SHOWN / PREVIEW_HIDDENPreview appeared / disappeared
PREVIEW_CLICK_SHOWPreview tapped and expanded
PREVIEW_CLICK_CLOSEPreview close button tapped
The payload shape differs by platform iOS delivers a JSON string (String?); Android delivers a Map (Map<String, Any?>). If you want shared logic, normalize to one shape in your app.
Reference

Error Handling

Errors arrive through onError (delegate / handler) and, on iOS, the collection-specific onListViewError. Both platforms use the same code scheme.

Code ranges

RangeMeaning
9000sInitialization and setup problems — something your app must fix
9300sEncoding — mostly the Editor SDK
9500sUnsupported media or feature
9900sNetwork and parsing
10000Unclassified exception

Errors you'll actually meet

CodeNameCause · fix
9000NOT_INITIALIZED_ACCESS_KEYThe SDK was called before setAccessKey → move it to Application.onCreate/AppDelegate
9001NOT_INITIALIZED_SHARE_URLNo share URL configured → check Shoplive Admin
9002NOT_INITIALIZED_CONFIGShorts configuration not received yet → check the network and retry
9050REQUIRED_COMPONENT_ACTIVITY(Android) A non-ComponentActivity context was passed → pass the activity, not applicationContext
9310REMOVED_VIDEOA deleted or private short was opened → validate the shortsId in your deep link
9501UNSUPPORTED_MEDIAA codec the device can't play
9900FAILED_NETWORKNetwork failure → offer a retry
9901FAILED_JSON_PARSINGResponse parsing failed → suspect an SDK/server version mismatch
9902FAILED_FETCH_DATA(Android) Query failed → check the query options and permissions
10000UNEXPECTED_ERRORUnclassified → contact us with the logs
func onError(error: Error) {
    guard let e = error as? ShopLiveCommonError else { return }
    log("shortform error \(e.codes) \(e.message ?? "")")
    // 9900s → offer a retry; 9000s → assert during development
}
override fun onError(context: Context, error: ShopLiveCommonError) {
    log("shortform error ${error.code} ${error.message}")
    when (error.code) {
        ShopLiveCommonErrorCode.FAILED_NETWORK -> showRetry()
        ShopLiveCommonErrorCode.NOT_INITIALIZED_ACCESS_KEY -> check(false) { "access key not set" }
        else -> Unit
    }
}
Don't swallow errors An empty onError makes a failing collection look like an empty collection, with no way to tell why. Log it at minimum, and make it visible (a toast, say) in development builds.
Reference

Client-Configured Items

These live in Shoplive Admin or your backend, not in code. When they're missing, correct code still shows an empty screen.

ItemWhereWhy it matters
accessKeyIssued by ShopliveRequired by every call (quick start)
Shorts content registeredShoplive AdminWithout video, thumbnail and title there is nothing to list
Tags · brands · collectionsShoplive AdminMust match your query strings exactly
Product links (SKU, product URL)Shoplive AdminDrives the product card and routing
Share URLShoplive AdminMissing it produces 9001
JWT secretYour Shoplive contact → your backendConnect user identity. Never ship it in the app
Deep link domainYour sideSo shared links come back into the app
Tag and brand strings are unforgiving Register spring-new in Shoplive Admin and query spring new and you get nothing. Keep them in shared constants so both sides use the same value.
Reference

Troubleshooting & FAQ

The collection is empty

In order: ① did you call submit() (Mission 2) ② is the query too narrow — tags, brands and SKUs combine with AND ③ does Shoplive Admin actually have shorts matching it (configuration) ④ what does onError report?

Only thumbnails show, no video (Android)

Most likely the playback engine module (shoplive-media3 or shoplive-exoplayer) isn't in your dependencies. The shortform module references it as compileOnly, so your app must declare it (install).

Likes don't stick between sessions

There is no user identity. Call setAuthToken or setUser before the user reaches a shorts screen (Mission 4).

The share sheet is hidden behind the shorts screen (iOS)

The full-screen feed lives in the SDK's own UIWindow. Present relative to the window from ShopLiveShortform.getCurrentKeyWindow() (Mission 5).

The preview stays on screen after I navigate away

The preview is a separate layer floating above the app, so navigation doesn't remove it. Call hidePreview() (Android) or close() (iOS) when leaving (Mission 3).

Scrolling to the end keeps requesting more pages

The last page didn't return hasMore = false (Mission 6).

Audio keeps playing after I switch tabs

Collection autoplay was left on when the screen went away. Call disablePlayVideos() in onPause/viewDidDisappear (Mission 2).

I want users to create and upload shorts

That's outside this SDK. See the Shortform Editor SDK guide.

Can I use this alongside the Live (Player) SDK?

Yes. The accessKey and user identity are set once in the shared module and both SDKs use them. Just keep the two SDKs on matching versions.

Shoplive Shortform SDK Integration Guide · based on iOS 1.8.13 / Android 1.8.14 · Docs