Dilshat Rakhimov
May 2026 · 15 min read

A SuperApp has an unpleasant technical problem that is usually underestimated until marketing starts promoting individual products. The app can be huge: deposits, loans, transfers, utility bills, mobile top-up, tickets, insurance, eGov-like flows, partner storefronts, and dozens of WebView services. But the ad link still opens either the home screen, the last open screen, a WebView without authorization, or the correct service - but without UTM, MMP payload, and campaign context.

At the user level it looks like a small UX issue: someone clicked an ad for a deposit and landed on the generic app home. At the performance-marketing level it is a systemic failure. The campaign is buying intent for a specific product, but the app does not bring the user to that product. The algorithm gets a weak post-click signal. Attribution sees the install or open but loses the service-level outcome. The product team says ads do not work. Marketing replies that the events are firing. In reality, the route is broken.

A deep link in a SuperApp is not just a URL. It is a product API.

Deep links for SuperApps

Why this hurts particularly hard inside a banking SuperApp

In ecommerce, a deep link usually points to a product, a category, or a checkout. In a banking SuperApp the link target is more complex:

  1. The user may not be installed, installed but not authorized, authorized but without KYC, KYC-approved but not eligible for the product.
  2. The right screen may be a native module, an embedded WebView, a partner domain, a mini-app, or a legacy web service.
  3. The WebView may require separate authorization or a cross-token.
  4. The campaign may come through Google Ads, Meta, TikTok, push, SMS, QR, email, or an MMP link.
  5. Attribution parameters can live in the URL, in an MMP callback, in the Android install referrer, in SKAN/postback, in a backend click context, or in a CRM event.

If the architecture is built as "catch a link, open the app", it will not survive that many transitions. You need a link contract: stable templates, verified domains, a context ledger, an auth handoff, and rules for what parameters can travel between native and WebView.

Field observation on Halyk/Homebank: it is not only assetlinks.json

I would not write "Halyk has no App Links" in such a blanket form. A quick public check on May 4, 2026 shows a more accurate picture:

EndpointWhat you see publicly
https://homebank.kz/.well-known/assetlinks.jsonReturns JSON and lists Android package kz.kkb.homebank
https://homebank.kz/.well-known/apple-app-site-associationReturns an AASA file with applinks and broad paths *, /
https://halykbank.kz/.well-known/assetlinks.jsonReturns an HTML page, not Digital Asset Links JSON
https://www.halykbank.kz/.well-known/assetlinks.jsonReturns an HTML page
https://www.homebank.kz/.well-known/assetlinks.jsonReturns 404
https://halykbank.kz/.well-known/apple-app-site-associationReturns an HTML page, not AASA JSON

This is not a full reverse-engineering audit of the app. For that you would have to look at the Android manifest, the iOS associated domains entitlement, the MMP configuration, runtime routing, and actual device behavior. But as a public symptom this is enough: the link estate is uneven. One domain may be correctly associated with the app, while other marketing or corporate domains are not. If ad, landing, SMS, QR, or WebView links bounce between domains like these, some routes will open in the browser, some in the app, some via redirect, and some will lose parameters.

And even when a verified host is in place, that does not solve the second layer of the problem: you also need template links to specific products and services. A link like https://homebank.kz/ may technically open the app. But it does not tell the app "open the KZT deposit category", "open mobile top-up with provider_id", "open the insurance WebView and preserve campaign_id".

Verified links are layer 0, not the whole system

On Android, App Links work through intent filters and Digital Asset Links. Android verifies the hosts listed in intent filters and looks for a file at https://hostname/.well-known/assetlinks.json. If the host fails verification, the system falls back to ordinary intent resolution and the link may land in the browser or open a chooser.

On iOS, Universal Links require the Associated Domains entitlement and an apple-app-site-association file on each domain or subdomain that the app declares as applinks:. Apple specifically warns that different subdomains need their own association files, and that redirects for AASA hosting are not supported. This matters for banks: example.kz, www.example.kz, go.example.kz, online.example.kz, pay.example.kz are different hosts, and you cannot rely on a single file to magically cover them all.

Minimal Android contract:

<activity android:name=".MainActivity" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="go.bank.kz" />
</intent-filter>
</activity>

Minimal assetlinks.json:

[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "kz.bank.superapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:..."
]
}
}
]

Minimal AASA:

{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.kz.bank.superapp"],
"components": [
{ "/": "/dl/v1/*" }
]
}
]
}
}

But in a SuperApp this is not enough. If go.bank.kz opens the app and from there the app cannot parse deposit_category, provider_id, service_id, campaign_id, ctx, and auth state, a verified link just becomes a fancy entry into the home screen.

Link contract: product templates instead of URL chaos

A SuperApp with hundreds of services should not support hundreds of random deep links. You need a small, versioned grammar:

Deep-link contract

A good set of templates:

https://go.bank.kz/dl/v1/deposits/{category}
https://go.bank.kz/dl/v1/deposits/{category}/offers/{offer_id}
https://go.bank.kz/dl/v1/payments/{provider_id}
https://go.bank.kz/dl/v1/transfers/card-to-card
https://go.bank.kz/dl/v1/loans/{product_id}
https://go.bank.kz/dl/v1/services/{service_id}
https://go.bank.kz/dl/v1/marketplace/{merchant_id}/items/{item_id}

Marketing examples:

https://go.bank.kz/dl/v1/deposits/term
?currency=KZT
&term=12m
&utm_source=google
&utm_medium=cpc
&utm_campaign=deposit_kzt_may
&utm_content=rate_hero
https://go.bank.kz/dl/v1/payments/mobile-kcell
?utm_source=push
&utm_campaign=topup_weekend
&ctx=3f0b4d...
https://go.bank.kz/dl/v1/services/insurance-travel
?utm_source=meta
&utm_campaign=travel_insurance
&deep_link_value=service_insurance_travel
&af_sub1=insurance

Rules:

  1. The path describes the destination: product, category, service id, merchant id.
  2. The query describes campaign context, filters, and safe UI hints.
  3. Never put national ID (IIN), card number, account number, contract number, phone, loan amount, private account id, or any PII in the URL.
  4. For sensitive details use ctx or state: a short opaque ID that the server resolves after authorization.
  5. Every template has an owner, a schema for parameters, a fallback, an analytics event, and a QA status.

The deep-link registry should not live in Notion or in the head of one mobile developer. It should be a managed config:

{
"template": "/dl/v1/payments/{provider_id}",
"owner": "payments",
"requires_auth": true,
"requires_kyc": true,
"surface": "native_or_webview",
"allowed_params": [
"utm_source",
"utm_medium",
"utm_campaign",
"utm_content",
"ctx",
"provider_id",
"deep_link_value",
"af_sub1",
"gclid",
"gbraid",
"click_id"
],
"fallback": "/payments",
"open_event": "deeplink_open",
"success_event": "payment_provider_opened"
}

Cross-authorization: where parameters get lost most often

The most common SuperApp failure looks like this:

  1. The user clicks an ad: ...?utm_campaign=deposit_kzt&deep_link_value=deposit_term.
  2. An App Link opens the app.
  3. The user is not authorized.
  4. The app sends them into login or KYC.
  5. After login the auth service returns the user to the default home.
  6. utm_*, deep_link_value, service_id, return_to, and MMP attributes are already gone.

Or the other variant:

  1. The app opens a service WebView.
  2. The WebView requires its own token.
  3. The cross-auth endpoint performs a redirect.
  4. The redirect does not pass query params through.
  5. The service opens, but the campaign context never reaches the purchase event.

The solution is not to drag every parameter through every redirect. For that you need a context ledger.

Attribution and auth handoff

Architecture:

campaign / MMP link
-> link gateway parses params
-> context ledger creates ctx_id
-> verified app link opens app with ctx_id + destination
-> app checks auth / KYC / eligibility
-> auth starts with state=ctx_id
-> app retrieves destination after auth
-> WebView receives signed handoff_token + ctx_id
-> service backend resolves ctx_id server-side
-> app/backend emits product events with attribution context

The point of ctx_id: it has to survive every transition. Raw UTM can be lost, the MMP callback may arrive later, the install referrer may only appear on first open, the WebView may launch after login. But ctx_id ties all of this into a single record.

Example record:

{
"ctx_id": "ctx_01HX...",
"created_at": "2026-05-04T10:15:00Z",
"destination": {
"template": "/dl/v1/deposits/{category}",
"category": "term",
"currency": "KZT",
"term": "12m"
},
"attribution": {
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "deposit_kzt_may",
"gclid": "hidden_or_hashed",
"mmp": "appsflyer",
"deep_link_value": "deposit_term"
},
"state": {
"requires_auth": true,
"requires_kyc": true,
"ttl_minutes": 60,
"consent_scope": "analytics_ads"
}
}

If the user is already authorized, the app router opens the destination immediately. If not, it stores the ctx_id, sends the user into login/KYC, and after success returns them not to home but to the same route target.

MMP and UTM: do not conflate routing and attribution

One reason SuperApp teams break deep links is that they try to use a single URL to solve two different jobs:

  1. Routing: where to open the user.
  2. Attribution: where the user came from.

These layers are related but not identical. deep_link_value=deposit_term may be a routing signal. utm_campaign=deposit_kzt_may is campaign reporting. af_sub1=deposit is an MMP sub parameter. gclid or gbraid is an ad click identifier. provider_id=mobile-kcell is a product target. You cannot just shovel all of this into every service, and you cannot afford to lose any of it at login.

For AppsFlyer OneLink, the installed-user UDL flow can return deep_link_value, deep_link_sub1-10, and parameters from the link. But for new users, privacy protection limits the payload: you cannot count on receiving every campaign field, such as media source and campaign, in the deep-link callback after install. Adjust has a similar split between link parameters, deep_link, and custom label/redirect logic. So the app router has to be able to merge multiple sources:

SourceWhat it providesHow to use it
Open URLPath, query, ctx, UTM, click idsPrimary routing for installed users
MMP callbackdeep_link_value, sub params, attribution metadataDeferred routing and attribution enrichment
Android Install ReferrerReferrer URL from Google PlayAndroid install attribution fallback
Backend click contextNormalized campaign params and destinationSource of truth for cross-auth and WebView
Firebase/GA4 eventsApp open, screen, product eventMeasurement and audience building

Merge rule:

destination:
ctx ledger > explicit deep_link_value > URL path > fallback route
campaign context:
backend click context > MMP attribution payload > open URL params > install referrer
sensitive data:
never from URL; resolve only after auth by ctx_id

A side note: do not start new projects on Firebase Dynamic Links. Firebase officially deprecated Dynamic Links, with shutdown scheduled for August 25, 2025. For installed-user deep linking use App Links and Universal Links directly. For deferred deep linking and paid attribution, use an MMP or your own link gateway with a clear fallback, but do not build a new system on FDL.

WebView services: how not to lose context inside a mini-app

A WebView inside a SuperApp often lives as a separate product within the product. It can have its own backend, its own sessions, a partner order id, and its own analytics script. If the native app simply opens:

https://partner-service.kz/start

the context is almost guaranteed to be lost. You need a handoff:

native app
-> POST /webview/handoff
ctx_id
service_id
user_session_id
return_to
<- handoff_token
-> open WebView:
https://service.bank.kz/start?ctx=ctx_01HX...&token=jwt...

Requirements for the handoff token:

  1. Short TTL: minutes, not days.
  2. Audience-bound: only the specific service id.
  3. Return path signed: the WebView should not decide on its own where to send the user back.
  4. No raw PII: the token may point at a server-side subject but must not reveal IIN, phone, or account number.
  5. Event correlation: the token contains or allows lookup of an event_id / ctx_id.

When the WebView completes an action, it should return the event to native/backend:

window.ReactNativeWebView?.postMessage(JSON.stringify({
type: 'service_purchase_success',
ctx_id: 'ctx_01HX...',
service_id: 'insurance-travel',
transaction_id: 'svc_983421',
event_id: 'evt_01HX...'
}));

But the final measurement event should be confirmed by the backend:

WebView callback
-> service backend validates transaction
-> core backend deduplicates event_id
-> value model calculates commission/margin
-> Firebase/GA4/MMP event receives product_type + ctx attribution

Otherwise the purchase can land in analytics before the payment has actually been confirmed.

What links should look like for deposits, payments, and services

Bad link:

https://homebank.kz/

It is not addressable. The ad platform does not know which product was promoted. The app does not know where to send the user. BI then tries to guess intent from the campaign name.

Weak link:

https://homebank.kz/deposits

It is better, but the category, currency, offer id, route fallback, and attribution context are missing.

Acceptable link:

https://go.bank.kz/dl/v1/deposits/term
?currency=KZT
&term=12m
&source_surface=paid_search
&utm_source=google
&utm_medium=cpc
&utm_campaign=deposit_term_kzt
&utm_content=rate_18
&ctx=ctx_01HX...

For payments:

https://go.bank.kz/dl/v1/payments/mobile-kcell
?utm_source=push
&utm_campaign=mobile_topup_may
&ctx=ctx_01HX...

For a WebView service:

https://go.bank.kz/dl/v1/services/travel-insurance
?utm_source=meta
&utm_campaign=travel_season
&deep_link_value=service_travel_insurance
&af_sub1=insurance
&af_sub2=travel
&ctx=ctx_01HX...

For an MMP OneLink you can hold the route in deep_link_value, and the parameters in sub fields:

https://brand.onelink.me/AbCd
?pid=googleadwords_int
&c=deposit_term_kzt
&deep_link_value=deposit_term
&deep_link_sub1=KZT
&deep_link_sub2=12m
&af_sub1=deposit

But the app still has to normalize this into its own route:

{
"template": "/dl/v1/deposits/{category}",
"category": "term",
"currency": "KZT",
"term": "12m"
}

What to do when the app is not installed

If the app is installed, a verified link should open it. If it is not installed, the link should open a fallback: App Store, Google Play, or a web landing page. An MMP is usually helpful here because it can route-by-platform and supports deferred deep linking.

But you cannot count on the App Store or Play Store preserving the full URL verbatim. On Android there is the Install Referrer API, which can return the referrer content from Google Play. On iOS, the direct referrer through the install flow is fundamentally limited, so deferred deep linking mostly relies on MMP attribution matching and a privacy-safe payload.

The right model:

  1. For installed users - App Links / Universal Links.
  2. For not installed - an MMP link or link gateway with a store fallback.
  3. For deferred routing - deep_link_value + sub params, not a hope of a full raw URL surviving install.
  4. For paid attribution - MMP/ads integration, not a homemade UTM spreadsheet.
  5. For bank compliance - consent-aware storage and deletion of click context.

QA matrix: how to know deep links actually work

You should not just test "the link opens the app", you should test the whole route.

ScenarioExpected result
Installed + logged inOpens the exact product screen
Installed + logged outLogin, then the same product screen
Installed + KYC missingKYC flow, then return_to or a clear eligibility fallback
Not installed AndroidStore install, first open, deferred route
Not installed iOSStore install, first open, MMP deferred route where available
WebView serviceCross-auth token is present, ctx is preserved, service event returns
Expired WebView tokenRefresh/handoff retry, not home screen
Invalid provider_idControlled fallback to category screen
utm_* + MMP paramsContext ledger holds both layers
Sensitive params in URLLink rejected or sanitized

Commands for technical QA:

curl -i https://go.bank.kz/.well-known/assetlinks.json
curl -i https://go.bank.kz/.well-known/apple-app-site-association

Checks:

  1. Status 200.
  2. No redirect for association files.
  3. Correct JSON body.
  4. Correct content type where the platform expects it.
  5. Every claimed host has its own file.
  6. Production app id/package and signing certificate are present.
  7. Dev/staging app ids do not accidentally shadow production behavior.

Android test:

adb shell am start \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "https://go.bank.kz/dl/v1/payments/mobile-kcell?ctx=test"

iOS test:

  1. Add the URL to Notes.
  2. Long-press it and verify that iOS offers to open in the app.
  3. Use Associated Domains diagnostics in Developer settings.
  4. Check that directly typing a URL into Safari's address bar is not treated as proof of Universal Link behavior. Apple documents that entering a URL directly into the address bar does not open the app as a Universal Link.

Measurement events

Deep-link success should be measured separately from product success.

deeplink_received
ctx_id
route_template
product_type
source_surface
deeplink_resolved
destination
requires_auth
requires_kyc
fallback_used
deeplink_opened
surface: native | webview
service_id
latency_ms
deeplink_failed
reason: host_unverified | invalid_template | auth_lost | service_not_found
product_value / purchase / generate_lead
ctx_id
event_id
value
currency
attribution_context

In BigQuery you should be able to answer:

  1. Which campaigns lose the route most often after auth?
  2. Which WebView services open but never return a callback?
  3. Where are most invalid provider ids coming from?
  4. Which utm_campaign values produce deeplink_received but not deeplink_opened?
  5. How many product purchases carry a ctx_id, and how many look organic because the context was dropped?

What I would force a SuperApp team to do in 30 days

  1. Pick a canonical link domain: for example go.bank.kz, or a verified homebank.kz, but without mixing in non-associated host variants.
  2. Configure App Links / Universal Links for production, staging, and dev separately.
  3. Remove marketing links from domains whose association endpoints return HTML/404.
  4. Build a registry of deep-link templates for deposits, payments, transfers, loans, cards, marketplace, services.
  5. Add a ctx_id ledger and stop carrying sensitive context in the query string.
  6. Rebuild login/KYC flow so that state=ctx_id survives auth.
  7. Implement a WebView handoff token and a return_to contract.
  8. Normalize MMP payloads into the same route contract.
  9. Log deeplink_received, deeplink_resolved, deeplink_opened, deeplink_failed.
  10. Build a QA dashboard for route loss, auth loss, WebView callback loss, and attribution loss.

The main point

If a SuperApp cannot open a specific deposit, payment, loan, or WebView service through a stable link, marketing is doomed to optimize toward an abstract app open instead of a product. If cross-authorization loses URL parameters, a paid campaign can deliver the right user but the purchase event will look like organic or generic app usage. If the MMP payload lives separately from the native router, deferred deep linking becomes a matter of luck.

Deep linking in a SuperApp should be designed as growth infrastructure: verified hosts, a template registry, a context ledger, an auth-safe return path, a WebView handoff, MMP merge rules, and measurement events. Then a link from an ad, push, SMS, or QR leads not "into the app", but into the right product with the attribution context preserved.

Sources

FAQ

Is a correct assetlinks.json enough for deep links to work?

No. Verified App Links and Universal Links only guarantee that the OS opens your app. They say nothing about where the user lands or which parameters survive cross-authorization, the login round trip, the KYC detour and the WebView handoff.

Should MMP parameters drive routing?

No — routing and attribution are separate jobs. MMP parameters answer where an install came from; the context ledger answers where this user should land. Conflate them and you break both.

How do I properly test a Universal Link on iOS?

Add the URL to Notes, long-press it and confirm iOS offers to open it in the app, then use the Associated Domains diagnostics in Developer settings. Typing the URL into the Safari address bar is not a valid test — Apple documents that this does not open the app as a Universal Link.