The requirement, stated plainly
Meta's playable spec is unambiguous: the creative must be a single, self-contained HTML file. Concretely:
- One file, no exceptions — Meta's packaging rule caps the upload at a single file (
maxFiles: 1in the spec). No accompanying folder of images, audio clips, or scripts. - Everything embedded — every sprite, sound effect, and font lives inside that one HTML document, typically as base64 or base122 data encoded directly into the file rather than referenced by a path.
- Zero external requests — the spec prohibits external JS, CSS, image, font, or network calls once the file is running (
prohibitsExternalRequests: true). Nothing is fetched after load, or at all. - A size ceiling on top of that — 5 MB is the hard cap, 2 MB recommended, which matters more than it sounds because base64 encoding inflates asset size by roughly a third over the raw file.
Two more rules ride along but aren't about file shape: the CTA must call FbPlayableAd.onCTAClick() specifically — no window.open, no auto-redirect — and Meta prohibits the MRAID click pattern other single-file networks use. Those are click-through and SDK rules, covered in more depth on the Meta playable ads page. This page is only about the single-file part.
Why Meta most likely built the spec this way
Upfront: Meta's docs state the single-file requirement itself, but don't publish the reasoning behind it. This section is inferred from how Meta's ad delivery works, not a quote from Meta.
Meta serves playable creatives inside its own sandboxed ad container, across a feed-serving system operating at enormous, distributed scale — the same creative renders inside Facebook, Instagram, Messenger, and Audience Network placements, on wildly different devices and connections, sometimes ones where a second or third HTTP request isn't reliable. A single self-contained file collapses that variability into one guarantee: if the container can fetch and render one document, the creative works. No relative path resolves differently across serving contexts, no separate asset request gets blocked or cached inconsistently, and there's no dependency on the sandbox allowing outbound calls at all.
That lines up with why the rule is paired with a strict zero-external-requests policy rather than just a file-count cap. A sandboxed container built for scale and security has an obvious incentive to disallow arbitrary outbound calls from creative code entirely — that closes off tracking, redirect, and reliability problems in one move. Once external requests are off the table, a multi-file bundle with relative paths stops being useful anyway, so requiring one embedded file reads less like a separate decision and more like a direct consequence: simpler to cache, sandbox, and serve consistently at that scale. Treat this as informed reasoning, not confirmed Meta policy.
What this means for a Unity Playworks/Luna export
This is where the requirement bites. A Playworks/Luna export is not single-file by default — a typical export (commonly landing in ProjectRoot\LunaTemp\stage4\create-hub) is an HTML entry point sitting alongside separate image, audio, and sometimes script files, referenced by ordinary relative paths. That's normal for a playable running in a browser, and it's exactly the shape Meta's spec doesn't accept.
Shipping that export to Meta unmodified doesn't just risk a slow load — it fails outright, since the container has no separate files to find and no permission to fetch them anyway. Getting to something Meta will accept means converting every separate asset into embedded data inside the HTML document, removing every relative path and external call, and keeping the result under the byte cap even after base64 inflates it. Doing that by hand for every asset, and re-doing it on every build change, is real work that's easy to get subtly wrong in ways that only surface as a rejection later.
How PlayableKit handles the conversion automatically
PlayableKit doesn't change how you build in Unity — you still export from Playworks/Luna exactly as you do today. When Meta is one of the networks you select, PlayableKit takes that same multi-file export and inlines every asset it references as an embedded data URI inside a single meta.html document, strips anything that would count as an external request, and checks the result against Meta's 5 MB cap (aiming for the recommended 2 MB) before handing it back. The same source build gets packaged correctly for AppLovin, Unity Ads, Vungle, Google Ads, Mintegral, and TikTok in parallel, each to that network's own required shape — see the single-file vs ZIP comparison for who wants which format, and the HTML5 playable converter page for what else changes inside the file.