What FbPlayableAd.onCTAClick() is, and why Meta specifically requires it
FbPlayableAd is Meta's own playable SDK object, injected into the ad by Meta's container at serve time — it's not something you build or bundle yourself. onCTAClick() is its designated click-through hook: the single method Meta's wrapper listens for to know the user tapped your call-to-action and the click should be attributed and routed to the store. PlayableKit's network spec records this exactly — for Meta, clickApi: "FbPlayableAd.onCTAClick()", with an explicit note that the CTA must call it, and must not auto-redirect or fall back to window.open().
This is stricter than it looks. A creative that renders inside AppLovin's or Unity Ads' surface can get away with an MRAID mraid.open() call; one aimed at Google can rely on ExitApi.exit() or a bare clickTag. Meta's surface recognizes none of those. The ad is running inside a container that only wires up its click attribution and store routing when it sees the actual FbPlayableAd.onCTAClick() call — a generic click listener, an anchor tag, or a raw window.open() call all render fine but go nowhere, because nothing on Meta's side is watching for them.
Common specific mistakes
1. Calling a different function
The most common cause is leftover wiring from another network. A build adapted from an AppLovin or Unity Ads export may still call mraid.open(url); one adapted from a Google export may still expect clickTag and ExitApi.exit(). Both run without error inside a browser, which is exactly why the mistake survives local testing — it only surfaces once Meta's actual review or delivery pipeline finds no attributed click.
2. Attached to the wrong DOM element or event
The call can be written correctly and still never execute if it's bound to the wrong thing — an engine-internal event that doesn't correspond to a real user tap, a debug/test button that only exists in a local harness and gets stripped from the production build, or a click handler attached to a container element that sits behind the actual visible button in the render stack.
3. Calling before the container or SDK is ready
window.FbPlayableAd only exists once Meta's wrapper has injected it into the page. A call fired during initial script execution, before that object is present, hits undefined and throws (or silently no-ops if the surrounding code swallows the error) instead of registering the click.
4. Wrapped in a condition that never evaluates true
A guard like if (window.FbPlayableAd) is good practice, but it's easy to get the property name wrong, check for a global that only exists in a different network's environment, or leave a leftover targetPlatform === "playground"-style flag from local testing that never matches once the network target is actually set to Meta. Any of these silently prevent the real call from ever running, with no error to flag it.
How to fix and verify it
Because window.FbPlayableAd is only injected inside Meta's real ad container, it won't exist in a plain local browser test — so the fix isn't just "click the button and see." Verify it directly:
- Log the check itself:
console.log(typeof window.FbPlayableAd, typeof window.FbPlayableAd?.onCTAClick)right before the CTA fires, so you can see in the console whether the object is present at call time rather than assuming it. - Wrap the call in a try/catch and log both the attempt and any thrown error, so a silent failure shows up during local testing instead of only surfacing after Meta's review.
- Search the exported HTML for the literal string
FbPlayableAd.onCTAClick— if it isn't present at all, no code path reaches it regardless of how the button behaves visually. - Confirm nothing else in the file calls
window.open(),mraid.open(), orExitApi.exit()as the CTA's actual click handler — any of those running instead of (or before)onCTAClick()is the same failure with a different symptom.
PlayableKit's own conversion step handles this wiring directly rather than leaving it to a hand edit. When you package a build for the Meta target, its adapter script guards the call behind an explicit runtime check — if (window.FbPlayableAd && typeof window.FbPlayableAd.onCTAClick === "function") { window.FbPlayableAd.onCTAClick(); return; } — inside a try/catch, and only takes that branch when the network target is actually set to "meta". Because the check happens at call time rather than assuming the object exists on load, it avoids the "called before ready" failure above. PlayableKit's Meta packaging step then validates the output itself, checking the exported HTML for the literal FbPlayableAd.onCTAClick string as one of its compliance checks — so a build that's missing the call, or has it overwritten by a stale handler, fails that check before you ever submit it.
window.FbPlayableAd won't exist, so PlayableKit's adapter falls through to a generic fallback (MRAID or window.open) using whatever store URL you passed in. That fallback is useful for confirming your button is wired to a real tap during local testing — but it's not proof onCTAClick() itself fires. Only the literal call showing up in the exported HTML, verified against Meta's own test tools, confirms that.