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.json declaring Playable_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.html and config.json themselves cannot be nested.

A correct structure, concretely

Here's what a compliant ZIP looks like once unzipped:

my-playable.zip ├── index.html ← entry file, must be first-level ├── config.json ← must be first-level, declares orientation └── assets/ ← optional, for anything not inlined

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 and openAppStore() replace that entirely. A creative built around MRAID calls for another network won't click through correctly here.

Step-by-step packaging checklist

  1. Confirm your HTML entry file is named index.html — not game.html, not the Unity/Playworks/Luna default filename.
  2. Write a config.json with a valid Playable_orientation value, and place it next to index.html.
  3. Add the Playable-sdk.js script tag to index.html, before any code that calls SDK methods.
  4. Replace any MRAID or generic click-through calls with window.openAppStore().
  5. Strip or inline every external reference — no remote scripts, images, fonts, or network calls.
  6. Zip index.html, config.json, and any asset folders directly — do not zip a parent folder containing them.
  7. Check the final ZIP size is under 5 MB after compression.
Common failure: zipping the whole project folder in Explorer or Finder usually produces a ZIP with everything nested one level down. Select 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.