The real required structure
TikTok's spec is specific and easy to get wrong if you're used to packaging for other networks: index.html and config.json must both sit at the first level of the ZIP, not inside a subfolder. This trips people up especially if they've packaged for Mintegral before, where the convention is the opposite — Mintegral wants everything nested one level down inside a folder that matches the ZIP and HTML filenames. TikTok wants no such wrapper folder at all.
- The ZIP must be 5 MB or smaller after compression.
- The main HTML file, in the first-level directory, must be named exactly
index.html. - The first-level directory must also contain a
config.jsondeclaringPlayable_orientation. - Any other assets (images, audio, fonts) that aren't embedded inline can live in first-level or nested folders alongside these two files — but
index.htmlandconfig.jsonthemselves cannot be nested.
A correct structure, concretely
Here's what a compliant ZIP looks like once unzipped:
Compare that with what fails review: a ZIP that unzips into my-playable/index.html and my-playable/config.json — i.e. everything wrapped in a top-level folder — is rejected on structure alone, even if the playable itself works perfectly in a browser.
The SDK script tag and where it goes
TikTok requires its own Playable SDK to be present in the HTML: a <script src="Playable-sdk.js"></script> tag referencing the SDK file, loaded before your game code calls into it. The click-through has to go through TikTok's own API — window.openAppStore() — rather than a generic MRAID call or a raw window.open. If the SDK script tag is missing, malformed, or loaded after your CTA logic tries to call openAppStore(), the click-through silently fails even though the ad renders fine.
What's prohibited
- External requests. TikTok prohibits dynamic loading of external material and JS redirects — every asset your playable touches at runtime needs to already be inside the package, not fetched from a CDN or third-party host.
- MRAID format. TikTok's package is explicitly not an MRAID creative. Don't wire up
mraid.open()or wait on MRAID's ready event — TikTok's own SDK andopenAppStore()replace that entirely. A creative built around MRAID calls for another network won't click through correctly here.
Step-by-step packaging checklist
- Confirm your HTML entry file is named
index.html— notgame.html, not the Unity/Playworks/Luna default filename. - Write a
config.jsonwith a validPlayable_orientationvalue, and place it next toindex.html. - Add the
Playable-sdk.jsscript tag toindex.html, before any code that calls SDK methods. - Replace any MRAID or generic click-through calls with
window.openAppStore(). - Strip or inline every external reference — no remote scripts, images, fonts, or network calls.
- Zip
index.html,config.json, and any asset folders directly — do not zip a parent folder containing them. - Check the final ZIP size is under 5 MB after compression.
index.html, config.json, and any asset folders themselves — not their parent directory — before compressing.
How PlayableKit generates this automatically
PlayableKit takes a Unity Playworks/Luna export and, when TikTok is selected as a target, produces this exact structure without any manual re-zipping: index.html and config.json are written at the first level, the Playable-sdk.js script tag is inserted with the correct placement, MRAID-style click calls are swapped for window.openAppStore(), external references are resolved or inlined, and the whole thing is checked against TikTok's 5 MB limit before it's handed back to you as a ready-to-upload ZIP.