1. Missing or invalid config.json

TikTok is one of the only major networks that requires a separate manifest file — a first-level config.json declaring Playable_orientation, sitting right next to index.html. Google Ads, Meta, AppLovin, Unity Ads, and Vungle have no equivalent file requirement, which is exactly why this trips up teams shipping to TikTok after already shipping successfully everywhere else: a checklist that worked for five other networks silently fails here, because none of them ever asked for this file. If config.json is absent, nested in a subfolder instead of first-level, or syntactically invalid JSON, TikTok's automated review rejects the package before a human ever sees the creative.

Fix: see the full walkthrough at TikTok playable missing config.json: how to fix it for the exact required shape and a working example.

2. Leftover MRAID calls from another network's adapter

TikTok prohibits the MRAID format outright — not as a fallback option, as an outright ban. That matters because MRAID is the common convention AppLovin and Unity Ads build around, so a creative that started life as an AppLovin or Unity Ads package often still carries mraid.open() calls, an MRAID-ready event listener, or MRAID-specific sizing logic in its code path even after the CTA has been nominally rewired for TikTok. Those leftover calls don't stop the playable from rendering in a browser — they just aren't what TikTok's review pipeline is checking for, and their presence alongside (or instead of) the correct call is a common source of rejection.

Fix: treat a TikTok build as its own target rather than a reused MRAID package with the CTA swapped. Strip MRAID API calls and MRAID-ready gating entirely rather than leaving them as unused dead code. See why TikTok prohibits MRAID for the reasoning behind the ban and what to remove.

3. openAppStore() not firing due to SDK script order

TikTok's click-through is a single sanctioned call — window.openAppStore() — but it only exists once TikTok's own Playable-sdk.js has loaded and initialized. If the CTA's click handler is wired before the SDK script has run, or the SDK script tag is missing, misplaced, or loaded after the button becomes clickable, calling openAppStore() either throws a silent reference error or does nothing at all. The playable looks completely normal in a local preview — the button renders, the tap registers — right up until the store never opens, which is precisely why this one tends to surface only in TikTok's own review or in live testing, not on your desk.

Fix: confirm Playable-sdk.js is loaded locally and executes before any code path that can call openAppStore(), and that nothing shadows or redefines the function afterward. Full breakdown at openAppStore() not working.

4. Size overage after compression

TikTok's cap is 5 MB measured after compression — the zipped archive you actually upload, not the raw folder of source assets. Unity Playworks/Luna exports that bundle their own runtime, individual texture files, and per-clip audio can sit comfortably under the cap unzipped and still land over it once packaged, especially once a config.json, a locally-hosted SDK script, and any inlined assets are added on top of the base export. A build that passed for a network with a more generous limit, or that was never actually zipped and measured before, is the usual way this one gets discovered late.

Fix: zip the final package and check its compressed size directly rather than estimating from the source folder — don't assume a build that "felt light" locally is actually under 5 MB once packaged.

5. External requests

TikTok's spec prohibits dynamic external material loading and JS redirects — every asset, script, and resource the playable touches at runtime has to already be inside the package. A CDN-hosted font, an image fetched by URL, an analytics ping fired mid-session, or a script that redirects the page are all violations, even if they're incidental leftovers from a build that was authored with looser assumptions about network access. This one behaves the same way as the SDK-timing issue above: it doesn't break local rendering (your browser happily fetches whatever it's told to), so nothing looks wrong until TikTok's review actually inspects what the package tries to reach outside itself.

Fix: audit every network call, fetch, and remote reference in the build, and inline anything the creative genuinely needs rather than loading it on demand. If nothing in your code intentionally does this, check for anything a pipeline or third-party asset injected automatically — it's an easy thing to miss precisely because it doesn't announce itself.

Worth knowing: none of the five causes above are about creative quality — they're mechanical checks against TikTok's own packaging rules, and its automation enforces every one regardless of how strong the game itself is. For the complete spec, see TikTok Playable Ads: requirements, formats & common errors.

Catching these before you submit

Every one of these five causes is a verifiable property of the package itself — config.json presence and placement, leftover MRAID calls, SDK load order ahead of openAppStore(), compressed archive size, and any external reference outside the ZIP. Running your build through PlayableKit's free Playable Ad Validator checks exactly these things against TikTok's spec before you spend a review cycle finding out the hard way, at no cost and with no signup.

If you're adapting a Unity Playworks/Luna export by hand for TikTok specifically, PlayableKit automates the config.json generation, first-level index.html placement, Playable-sdk.js injection, window.openAppStore() CTA wiring, and MRAID/external-request stripping directly from your source build, then checks the zipped result against the 5 MB cap before you upload. See pricing for which plans include TikTok.