What AppLovin actually requires

AppLovin's spec is specific: a single self-contained HTML file, 5 MB or smaller, MRAID 2.0 compliant, supporting both orientations, with every image, audio clip, font, and script embedded directly in that one file. prohibitsExternalRequests isn't a soft guideline — the review tooling actually opens the file in a sandboxed webview with network access cut off (or monitored) and flags anything that tries to reach out, whether that's a real asset load, a tracking pixel, or a script tag pointing at a CDN.

The rejection message is blunt but not very diagnostic — it tells you a request happened, not what made it or when. That's the whole problem: "external request" covers at least three unrelated failure modes, and only one of them is something you'd find by reading your own game code.

Cause 1: an asset is genuinely referenced, not embedded

The most common cause. Somewhere in the export, an image, audio file, video, or web font is loaded by a relative or absolute path — src="assets/logo.png", a CSS @font-face pointing at a hosted font, a video tag pulling from a CDN — instead of being inlined as a base64 data URI baked into the HTML. This slips through easily because most engines' web exports (Unity's included) default to referencing loose files in a folder next to the HTML; that's fine for a normal build, but AppLovin only ever receives the single HTML file, so anything that isn't embedded in it simply won't resolve — or worse, resolves to a request the reviewer's sandbox can see.

It's also easy to introduce by hand: pasting in a quick loading-screen image via an <img src="https://..."> tag, or leaving an analytics/CDN-hosted library tag (a font kit, an icon library, a lightweight framework loaded via <script src="https://cdn...">) in the HTML head from an early prototype.

Cause 2: leftover third-party script in the source export

The second common cause is code that was never meant to ship: an analytics SDK, a crash reporter, an ad-mediation test harness, or a debug console left wired into the Unity project that gets baked into the Playworks/Luna export. None of this is visually obvious when you play the build — it fires silently in the background, often only on specific triggers (an error, a level-complete event, a timer) — so it can pass casual QA and still get caught by AppLovin's automated request monitoring, which watches for any outbound call regardless of whether it affects gameplay.

Cause 3 (the non-obvious one): your CDN is injecting a script after conversion

This is the one worth knowing about, because it will not show up no matter how many times you re-read your Unity project or the converted HTML on your own machine — because the file on your machine is clean. The problem happens after conversion, at the hosting layer, on the copy that gets served to whoever downloads it.

We hit this ourselves: a build that inlined every asset correctly and had zero external calls in its source still got rejected by AppLovin citing external requests. The root cause was Cloudflare Web Analytics' auto-injection feature. Cloudflare's proxy layer can automatically insert a small analytics beacon <script> tag into HTML responses as they pass through — and it decides whether to do that based on the response's Content-Type header. Because the download endpoint was serving the converted file as text/html, Cloudflare treated it as a normal web page and quietly injected the beacon script right before </html>, on the fly, on every download. The file that left our server was clean; the file that arrived in the browser — and the one that got zipped up and uploaded to AppLovin — had a script in it that neither we nor the advertiser ever wrote.

The fix was to stop giving Cloudflare a reason to touch the response: change the download route's Content-Type header to application/octet-stream instead of text/html, so the proxy layer treats it as an opaque binary download rather than a page to instrument. Any hosting setup that sits behind a CDN, reverse proxy, or corporate network filter with HTML-rewriting features (analytics auto-injection, ad-blocking bypass scripts, security banners, cookie-consent middleware) can do the same thing — the pattern generalizes well beyond Cloudflare specifically.

Why this is easy to miss: nothing in your source project, your build tool, or a local copy of the output file shows the injected script. It only exists in the response your CDN serves at request time — which is also, not coincidentally, the exact file that ends up in the ad network's hands.

How to actually diagnose which one you have

  1. Don't inspect a local copy — inspect the uploaded file. Download the file the exact same way it was submitted (through whatever link, portal, or export step feeds the ad network), then view-source on that specific file. Don't re-open the version sitting in your build output folder; it may genuinely differ from what got served.
  2. Search for anything you didn't intentionally embed. In that downloaded copy, search for <script, http://, and https:// and account for every hit. If you find a <script> tag right before </html> that isn't part of your game code, that's the injected-script pattern — check how the file was served (what Content-Type header, through what CDN/proxy) rather than your source project.
  3. If the hits are inside your own game code — an <img>, <audio>, @font-face, or analytics call with a real path or URL — that's Cause 1 or 2, and the fix is in the export itself: re-encode the asset as a data URI, or strip the leftover script.
  4. Check the response headers of the actual download, not just the file content. If it's served as text/html through a CDN with any kind of auto-injection or beacon feature enabled, that's worth ruling out even if the file content looks clean, since the CDN can rewrite it after your server sends it.

Fixing each category

  • Referenced asset (Cause 1): convert the file to a base64 data URI and inline it directly in the HTML — no relative paths, no CDN-hosted fonts or libraries.
  • Leftover third-party script (Cause 2): remove analytics/debug/mediation-test code from the Unity project before export, or strip it from the exported HTML if it can't be removed at the source.
  • CDN-injected script (Cause 3): serve the downloadable creative file with a neutral Content-Type such as application/octet-stream rather than text/html, and disable any auto-injection/beacon features on the CDN or proxy that serves it, so nothing gets added to the response after your build produced it.

PlayableKit's AppLovin output handles Cause 1 automatically — every asset is base64-encoded into the single HTML file as part of conversion — and downloads are served with a binary content type specifically so no proxy layer in the chain has a reason to rewrite the file. That still leaves Cause 2 (source-level tracking code) as something worth checking in your original Unity project, since PlayableKit converts what's in the export, not what should have been removed from it.