Fix AppsFlyer OneLink White Pages in TikTok's WebView
AppsFlyer OneLinks often render a white page inside TikTok's in-app browser. Here is why it happens and the proxy page that keeps attribution intact.

You put a OneLink in your TikTok bio or an ad, someone taps it, and they land on a blank white screen. The app never opens, the store never opens, and the attribution you were paying for never fires. This is not a OneLink misconfiguration. It is TikTok's in-app browser refusing to do the one thing the OneLink needs it to do, and the fix is a small page you host yourself.
The white page is TikTok's WebView, not your link
TikTok does not hand external links to Safari or Chrome. It opens them in an embedded WebView inside the TikTok app. Instagram, X, LinkedIn, and Facebook all do the same thing. That matters because Universal Links (iOS) and App Links (Android) were designed for links tapped in a real browser or from the home screen. They are not reliable inside the embedded WebViews social apps use, and they frequently do nothing at all.
A OneLink depends on that handoff. When it cannot fire, the redirect chain stalls and the user is left on whatever the WebView rendered, which is often nothing. On top of that, in-app browsers and ad redirect layers can alter, encode, or drop parts of a long tracking URL, so even the parts of the OneLink that would have survived can get mangled. AppsFlyer documented the same failure mode on X for iOS, where a WebView change left users stuck on a blank screen and organic conversions collapsed. The recommended recovery in that case was a controlled landing page, and the same approach fixes TikTok.
The fix: a proxy page you control
Instead of pointing TikTok at the raw OneLink, point it at a plain HTML page you host. That page renders fine inside TikTok's WebView because it is just content. It shows the thing the user came for, the product, the episode, the profile, and it carries one button. The button is what fires the OneLink, and it fires it in a way that breaks out of the WebView into the system browser, where the OneLink can actually resolve.
Host this page on a static host that serves it directly, Vercel or Netlify being the obvious picks. The URL of that page becomes your TikTok link. Nothing about the OneLink itself changes, you are only inserting one page in front of it that the WebView can survive.
Breaking out of the WebView
The button cannot just navigate to the OneLink in the current WebView, that lands you back on the white page. It has to open the OneLink in a fresh browser context. The mechanism differs by platform, so branch on it.
On iOS, a user tap on an anchor with target="_blank" is usually the most reliable way to push the flow out of TikTok's WebView into a cleaner browser context, where the OneLink resolves and hands off to the app or the store. On Android, the in-app WebView ignores _blank for this, so you use an intent:// URL that tells the OS to open the link in the system browser, with a fallback URL if nothing handles it.
<a id="open" href="#" rel="noopener">Watch the episode</a>
<script>
const oneLink = 'https://yourbrand.onelink.me/abc123?deep_link_value=episode&deep_link_sub1=ep_5&deep_link_sub2=my-billionaire-baby-daddy-married-my-grandmother'
const btn = document.getElementById('open')
const isAndroid = navigator.userAgent.toLowerCase().includes('android')
if (isAndroid) {
// Android webviews hand intent:// to the OS, target=_blank does nothing here
const bare = oneLink.replace(/^https?:\/\//, '')
btn.href = `intent://${bare}#Intent;scheme=https;action=android.intent.action.VIEW;` +
`category=android.intent.category.BROWSABLE;` +
`S.browser_fallback_url=${encodeURIComponent(oneLink)};end`
} else {
// iOS: a real tap on _blank is the reliable way out of the in-app webview
btn.href = oneLink
btn.target = '_blank'
}
</script>
The tap has to be a genuine user tap. Firing this from window.location or a timer on load does not count as user-initiated, and the browser ignores it. The button exists for exactly this reason, and it doubles as the moment the user chooses to leave TikTok, which is the behaviour you want anyway.
Do not host the proxy behind Cloudflare bot protection
This one costs people a day of debugging. The problem is not Cloudflare as a CDN, a static page on Cloudflare renders fine. The problem is Cloudflare's bot-protection layer: Managed Challenge, Super Bot Fight Mode, Turnstile, and JavaScript Detections. In-app WebViews send an atypical user agent and run a limited browser environment, so Cloudflare's scoring can read them as bot-like and serve a challenge page. Cloudflare's own documentation says challenges behave differently in embedded browser contexts and lists webviews among the environments it does not fully support. Inside TikTok that challenge can loop or hang, and you have traded a white page for a challenge screen.
Vercel and Netlify serve the page with no challenge in front of it by default, which is why they are the safer host for this one page. If you are committed to Cloudflare, exempt the proxy route from bot protection and challenges so the WebView is never handed one.
Attribution survives the jump, including deferred installs
The reason this is worth doing over a plain link is that the OneLink still does its job once it runs in the system browser. If the app is installed, it opens to the deep link target. If it is not, the user goes to the store, installs, and AppsFlyer delivers the deep_link_value on first open through deferred deep linking. Either way the attribution and the routing context that started on the proxy page carry through.
If the destination app is a Despia app, you do not parse any of this from the URL. Despia embeds the native AppsFlyer SDK, resolves the OneLink natively, and hands you the payload through a callback. Assign it bare, ideally in an inline <head> script before your bundle loads. Keep deep_link_value a short generic type and carry the specific record in deep_link_sub1, so your route map stays small and one link shape covers every episode or product.
// index.html, before your bundle. Assigned bare, no despia gate needed.
const ROUTES = { product: "/product", profile: "/profile", episode: "/episode" }
window.onAppsFlyerDeepLink = function (payload) {
const base = ROUTES[payload.deep_link_value] // "episode" -> "/episode"
if (!base) return // unmapped value, stay on the start page
const id = payload.deep_link_sub1 // the specific record, e.g. "ep_5"
// deferred first launch can fire this more than once, handle it once
const key = `af_dl_${payload.deep_link_value}:${id || ''}`
if (sessionStorage.getItem(key)) return
sessionStorage.setItem(key, '1')
window.location.assign(id ? `${base}/${id}` : base)
}
The matching OneLink carries the type, a stable id, and the display title:
...onelink.me/xxxx?deep_link_value=episode&deep_link_sub1=ep_5&deep_link_sub2=my-billionaire-baby-daddy-married-my-grandmother
Route on deep_link_sub1, the stable id, not on the title. deep_link_sub2 is cosmetic, use it for the visible slug or a heading, and let it change freely when the episode gets re-titled without breaking any link already out in the wild.
Put anything the route depends on in a deep_link_sub slot, not an invented key. On a deferred install AppsFlyer guarantees only deep_link_value and deep_link_sub1 through deep_link_sub10 in the resolved payload, so a custom key like episode_id can come back null on a fresh install even though it works for users who already have the app. The deep_link_sub fields are the ones that survive both paths.
Two more things the callback needs. Despia keeps the OneLink URL out of the WebView and never appends its query string to your app's URL, so reading window.location.search for these values inside the app gets you nothing. The callback is the contract. And on a deferred install the callback can fire more than once on first launch, which is what the sessionStorage guard above handles, so the route runs once no matter how many times the payload arrives.
That is the whole flow: a page you host absorbs the TikTok WebView, a user tap escapes to the system browser, the OneLink resolves, and Despia's native SDK fires your deep link callback with attribution attached. One page of HTML in front of the link you already have.
Add this to your app
The proxy page can be a route in the web app you already ship with Despia, and on the other side the native AppsFlyer SDK hands you the deep link through one callback, inside that same codebase. No second project, no native build to babysit.





