The rejection

This is a real rejection message AppLovin sends, and it's more literal than it sounds. It doesn't mean "some assets are missing" or "the file is too big." It means AppLovin's automated scan walked the HTML and found at least one reference to a binary resource — an image, an audio clip, a video frame, a font — that isn't sitting inline as base64 or base122 text. Somewhere in the markup or the JavaScript, there's a pointer to a file instead of the file's actual encoded contents.

The frustrating part is that the build can otherwise pass every other check: it's a single HTML file, it's under the size cap, MRAID is wired up correctly. One leftover reference is enough to fail this specific rule on its own.

What "single file" actually means to AppLovin

AppLovin's spec requires one self-contained HTML file with zero external requests — no separate JS, CSS, image, or font files, and no network calls at runtime. That requirement doesn't stop at the network boundary. It also rules out relative, file-system-style paths within the same package, because a ZIP-relative path is still a reference the browser has to resolve as a request, and AppLovin's serving pipeline doesn't guarantee that anything but the single HTML entry file survives.

So the only two forms a binary asset can legally take inside an AppLovin creative are:

  • A data: URI — the asset's bytes, base64-encoded, embedded directly in a src attribute or a stylesheet.
  • A base122-encoded blob — the same bytes, encoded into a denser near-ASCII text format, stored as an inline string (commonly a data-* attribute) and decoded back into a Blob/object URL at runtime via JavaScript already inside the file.

Either is fine. What isn't fine is anything in between — a path, a filename, a reference that assumes another file will be sitting next to the HTML when AppLovin serves it. It won't be.

How this usually sneaks in

MistakeWhy it fails
<img src="assets/hero.png"> left in the markupA relative path, not an encoded blob — AppLovin's scan flags it even if the image rendered fine in your local test, where the assets/ folder happened to still be there.
A build step that inlines "most" assetsAutomated inlining tools frequently miss assets loaded lazily, referenced from CSS background-image, or pulled in by a third-party library — one missed file is enough.
Video or audio referenced by <source src="...">Same rule applies to media as to images; a source path or a fetch() call to a local file both count as external references.
Fonts loaded via @font-face url(...)Font files are binary assets too — they need base64/base122 embedding exactly like images.

The fix

Treat this as a full audit, not a spot-check: every src, href, url(), and dynamically constructed path in the file needs to resolve to either a data: URI or an inline base122 string decoded at runtime — never a filename. Search the built HTML for anything that still looks like a file path (a string containing a slash and a file extension) inside a tag attribute or a script string; if you find one pointing at an asset, it needs to be re-encoded and inlined before resubmission.

This is exactly what PlayableKit automates for AppLovin exports. Every image, video frame, and audio asset from the source Luna/Playworks package is walked and inlined — base122-encoded into a data-* attribute with a small decoder that turns it back into a blob URL at runtime, so nothing in the final HTML is a file-system reference. The one deliberate exception is the Luna cache payload itself (scripts/jsons/blobs), which PlayableKit always re-encodes as base64 rather than base122 for AppLovin specifically — base122 is smaller, but it emits multi-byte characters that some ad-serving layers, AppLovin's included, don't render reliably even with a correct charset declared, which shows up as a black screen instead of a rejection. Base64 is the safer, encoding-agnostic choice for that particular payload. Either way, the output has no relative paths left for AppLovin's scan to catch.

For the deeper mechanics of how the brotli/base64/base122 pipeline compresses and re-encodes the underlying Luna cache, see the asset compression explainer — this article is deliberately just the AppLovin-specific slice of that picture.

Worth knowing: if you want to check whether an AppLovin build still has a stray file reference before resubmitting, PlayableKit's Playable Ad Validator scans for exactly this and reports it for free, no signup required.

For the full picture of what else AppLovin's review checks beyond encoding — MRAID readiness, click-through wiring, size limits — see building AppLovin playable ads.