What MRAID "ready" actually means

When AppLovin's SDK loads your HTML creative into a WebView, it injects its own mraid.js implementation and starts it in a loading state. Your creative's code and the MRAID bridge initialize at the same time but not necessarily at the same speed — the bridge needs to finish its native/JS handshake before any MRAID method is safe to call.

MRAID formalizes this with a state machine exposed via mraid.getState(): loading (container still initializing — calls aren't guaranteed to work), default (the container has fired its ready event and calls are safe), plus later states like expanded you don't usually need for a simple playable CTA.

The container signals the transition out of loading by firing a ready event, subscribed to with mraid.addEventListener("ready", callback). Calling mraid.open() before that event fires means calling into a bridge that isn't listening yet — depending on the SDK version and platform, that either throws, gets silently swallowed, or queues unpredictably. None of those are outcomes you want discovered during network review instead of during testing.

Common mistakes that cause this

1. Calling mraid.open() on page load or script execution

The click handler gets wired up as soon as the script runs, on the assumption that if window.mraid exists, it's usable. It exists as an object immediately — the object being defined and the container being ready are two different things. A tap in the first few frames after load can fire mraid.open() straight into the loading state.

2. Never checking mraid.getState() before calling

Some hand-written adapters call mraid.open() unconditionally, with no state check at all. This usually "works" in casual testing, since local testing tends to have MRAID ready before anyone taps anything, then fails intermittently on real inventory, on slower devices, or when a user taps unusually fast after the creative renders.

3. Race conditions between asset loading and MRAID init

If the click handler is attached inside an asset-loaded or game-start callback, you can hit the opposite trap: assets finish loading and the CTA becomes tappable before the MRAID bridge has fired ready, especially on a cold WebView. The fix isn't to guess which finishes first — it's to gate the mraid.open() call on the MRAID ready signal specifically, independent of whatever else is loading.

The fix: gate mraid.open() on the ready signal, not on load

The reliable pattern: attach the click handler whenever you want, but have the handler itself check state before calling into MRAID, and defer if it isn't ready yet.

  • If window.mraid doesn't exist at all, you're not in an MRAID container — fall back to a plain window.open(url, "_blank") so the creative still works when previewed outside an MRAID SDK.
  • If mraid.getState() is already past loading (typically default), call mraid.open(url) immediately.
  • If it's still loading, register a ready listener, and fire the call once the event lands.

This check belongs on the tap handler's execution path, not on page load. A user can tap the CTA at any point after the creative renders — sometimes before MRAID is ready, sometimes well after — so the check has to run at click time, every time, not just once up front.

Worth knowing: AppLovin's spec note is blunt about this — wait for MRAID ready before any MRAID API call or layout decision, not just mraid.open(). If your creative also calls mraid.getExpandProperties() or mraid.useCustomClose() on load, those need the same gate.

How PlayableKit's generated adapter handles this

When PlayableKit packages a build for AppLovin, the click-through wiring it injects follows exactly this pattern: the CTA handler checks whether window.mraid exists and exposes getState. If MRAID isn't present, it falls through to a plain window.open(). If it's present and getState() reports loading, the adapter registers a mraid.addEventListener("ready", …) callback and only calls mraid.open(url) once that event fires. If MRAID is already past loading when the tap happens, it calls mraid.open(url) immediately, with no artificial delay.

The store URL comes from whatever you supply as --android-url / --ios-url when generating the package — if neither is set, PlayableKit's compliance report flags MISSING_STORE_URL as a warning so it doesn't ship with a placeholder link.

If you're hand-editing a generated AppLovin creative

The adapter script PlayableKit injects is a small self-contained block marked with a data-playable-network-adapter attribute — meant to be left alone. If you're hand-editing afterward (a custom animation trigger, changing when the CTA becomes visible), check before shipping:

  • Nothing you add calls mraid.open(), mraid.expand(), or other MRAID methods directly, bypassing the existing ready check.
  • If you rewire the click target to a new element, the new handler still calls into the existing adapter function rather than a fresh, un-gated mraid.open() call.
  • Re-run the build through PlayableKit's free validator afterward — it checks that the AppLovin HTML contains a gated mraid.open call and no external script tags, which catches most hand-edit regressions before submission.