Deep links для SuperApp уровня Halyk: WebView-сервисы, cross-auth и UTM/MMP-атрибуция без потерь

Dilshat Rakhimov
May 2026 · 15 min read

У SuperApp есть неприятная техническая проблема, которую часто недооценивают, пока маркетинг не начинает продвигать отдельные продукты. Приложение может быть огромным: депозиты, кредиты, переводы, коммунальные платежи, мобильная связь, билеты, страховки, eGov-like сценарии, партнерские витрины и десятки WebView-сервисов. Но рекламная ссылка все равно открывает либо главную страницу, либо последний открытый экран, либо WebView без авторизации, либо правильный сервис, но без UTM, MMP payload и campaign context.

На уровне пользователя это выглядит как маленькая UX-проблема: человек нажал на рекламу депозита, а попал в общий экран приложения. На уровне performance-маркетинга это уже системная поломка. Кампания покупает intent на конкретный продукт, но приложение не доводит пользователя до этого продукта. Алгоритм получает слабый post-click сигнал. Атрибуция видит install или open, но теряет service-level outcome. Продуктовая команда говорит, что реклама не работает. Маркетинг отвечает, что события есть. На самом деле сломан маршрут.

Deep link в SuperApp - это не просто ссылка. Это продуктовый API.

Deep links for SuperApps

Почему это особенно больно в банковском SuperApp

В ecommerce deep link обычно ведет в товар, категорию или checkout. В банковском SuperApp link target сложнее:

  1. Пользователь может быть не установлен, установлен, но не авторизован, авторизован, но без KYC, KYC-approved, но не eligible для продукта.
  2. Нужный экран может быть native module, embedded WebView, партнерский домен, mini-app или legacy web service.
  3. WebView может требовать отдельную авторизацию или cross-token.
  4. Кампания может прийти через Google Ads, Meta, TikTok, push, SMS, QR, email или MMP link.
  5. Атрибуционные параметры могут жить в URL, MMP callback, Android install referrer, SKAN/postback, backend click context или CRM event.

Если архитектура построена как "поймали ссылку, открыли приложение", она не выдержит такого числа переходов. Нужен link contract: стабильные шаблоны, проверенные домены, context ledger, auth handoff и правила, какие параметры можно переносить между native и WebView.

Наблюдение по Halyk/Homebank: проблема не только в assetlinks.json

Я бы не писал "у Halyk нет App Links" в таком виде. Быстрая публичная проверка на 4 мая 2026 года показывает более точную картину:

EndpointЧто видно публично
https://homebank.kz/.well-known/assetlinks.jsonВозвращает JSON и содержит Android package kz.kkb.homebank
https://homebank.kz/.well-known/apple-app-site-associationВозвращает AASA-файл с applinks и broad paths *, /
https://halykbank.kz/.well-known/assetlinks.jsonВозвращает HTML-страницу, а не Digital Asset Links JSON
https://www.halykbank.kz/.well-known/assetlinks.jsonВозвращает HTML-страницу
https://www.homebank.kz/.well-known/assetlinks.jsonВозвращает 404
https://halykbank.kz/.well-known/apple-app-site-associationВозвращает HTML-страницу, а не AASA JSON

Это не полноценный reverse-engineering audit приложения. Для него нужно смотреть Android manifest, iOS associated domains entitlement, MMP configuration, runtime routing и actual device behavior. Но как публичный симптом этого достаточно: у link estate есть неравномерность. Один домен может быть правильно ассоциирован с приложением, а другие маркетинговые или корпоративные домены - нет. Если рекламные, лендинговые, SMS, QR или WebView-ссылки гуляют между такими доменами, часть маршрутов будет открываться через браузер, часть через приложение, часть через redirect, а часть будет терять параметры.

И даже когда verified host есть, это не решает второй слой проблемы: нужны шаблонные ссылки на конкретные продукты и сервисы. Ссылка вида https://homebank.kz/ технически может открыть приложение. Но она не говорит приложению: "открой категорию депозитов в KZT", "открой оплату мобильной связи с provider_id", "открой WebView страховок и сохрани campaign_id".

Verified links - это layer 0, не вся система

На Android App Links работают через intent filters и Digital Asset Links. Android проверяет хосты из intent filters и ищет файл по адресу https://hostname/.well-known/assetlinks.json. Если host не прошел verification, система возвращается к обычному intent resolution, и ссылка может уйти в браузер или показать chooser.

На iOS Universal Links требуют Associated Domains entitlement и файл apple-app-site-association на каждом домене или субдомене, который приложение заявляет как applinks:. Apple отдельно предупреждает, что для разных subdomains нужны свои association files, а redirects для AASA hosting не поддерживаются. Это важно для банков: example.kz, www.example.kz, go.example.kz, online.example.kz, pay.example.kz - это разные hosts, и нельзя надеяться, что один файл магически закроет все.

Минимальный 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>

Минимальный assetlinks.json:

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

Минимальный AASA:

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

Но в SuperApp этого недостаточно. Если go.bank.kz открывает приложение, а дальше приложение не умеет разобрать deposit_category, provider_id, service_id, campaign_id, ctx и auth state, verified link просто превращается в красивый вход на главную.

Link contract: продуктовые шаблоны вместо хаоса URL

SuperApp с сотнями сервисов не должен поддерживать сотни случайных deep links. Нужна небольшая, версионированная грамматика:

Deep-link contract

Хороший набор шаблонов:

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}

Примеры для маркетинга:

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

Правила:

  1. Path описывает destination: продукт, категорию, service id, merchant id.
  2. Query описывает campaign context, фильтры и безопасные UI hints.
  3. Нельзя класть в URL ИИН, номер карты, номер счета, номер договора, телефон, сумму кредита, приватный account id или любые PII.
  4. Для чувствительных деталей используется ctx или state: короткий opaque ID, который резолвится сервером после авторизации.
  5. У каждого шаблона есть owner, схема параметров, fallback, analytics event и QA status.

Deep-link registry должен жить не в Notion и не в голове одного mobile developer. Это должен быть управляемый конфиг:

{
"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: где чаще всего теряются параметры

Самая частая поломка в SuperApp выглядит так:

  1. Пользователь кликает рекламу: ...?utm_campaign=deposit_kzt&deep_link_value=deposit_term.
  2. App Link открывает приложение.
  3. Пользователь не авторизован.
  4. Приложение отправляет его в login или KYC.
  5. После login auth service возвращает пользователя на default home.
  6. utm_*, deep_link_value, service_id, return_to и MMP attributes уже потеряны.

Или другой вариант:

  1. App открывает WebView сервиса.
  2. WebView требует отдельный token.
  3. Cross-auth endpoint делает redirect.
  4. Redirect не прокидывает query params.
  5. Сервис открывается, но campaign context не доходит до purchase event.

Решение - не пытаться таскать все параметры через каждый redirect. Для этого нужен context ledger.

Attribution and auth handoff

Архитектура:

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

Смысл ctx_id: он должен пережить все переходы. Raw UTM может потеряться, MMP callback может прийти позже, install referrer может появиться только на first open, WebView может стартовать после login. Но ctx_id связывает все это в одну запись.

Пример записи:

{
"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"
}
}

Если пользователь уже авторизован, app router сразу открывает destination. Если нет, он сохраняет ctx_id, отправляет пользователя в login/KYC, а после success возвращается не на home, а в тот же route target.

MMP и UTM: не смешивайте routing и attribution

Одна из причин, почему SuperApp-команды ломают deep links, - они пытаются одним URL решить две разные задачи:

  1. Routing: куда открыть пользователя.
  2. Attribution: откуда пользователь пришел.

Эти слои связаны, но не равны. deep_link_value=deposit_term может быть routing signal. utm_campaign=deposit_kzt_may - campaign reporting. af_sub1=deposit - MMP sub parameter. gclid или gbraid - ad click identifier. provider_id=mobile-kcell - product target. Все это нельзя бездумно передавать во все сервисы и нельзя терять на login.

Для AppsFlyer OneLink installed-user flow UDL может вернуть deep_link_value, deep_link_sub1-10 и параметры из link. Но для new users privacy protection ограничивает payload: нельзя рассчитывать, что после install вы получите все campaign fields вроде media source и campaign в deep-link callback. Adjust аналогично разделяет link parameters, deep_link и custom label/redirect логику. Поэтому app router должен уметь объединять несколько источников:

ИсточникЧто даетКак использовать
Open URLPath, query, ctx, UTM, click idsPrimary routing для installed users
MMP callbackdeep_link_value, sub params, attribution metadataDeferred routing и attribution enrichment
Android Install ReferrerReferrer URL из Google PlayAndroid install attribution fallback
Backend click contextNormalized campaign params and destinationИсточник истины для cross-auth и WebView
Firebase/GA4 eventsApp open, screen, product eventMeasurement and audience building

Правило merge:

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

Отдельно: не начинайте новые проекты на Firebase Dynamic Links. Firebase официально deprecated Dynamic Links, а shutdown был назначен на 25 августа 2025 года. Для installed-user deep linking используйте App Links и Universal Links напрямую. Для deferred deep linking и paid attribution используйте MMP или собственный link gateway с понятным fallback, но не стройте новую систему на FDL.

WebView-сервисы: как не терять контекст в mini-app

WebView в SuperApp часто живет как отдельный продукт внутри продукта. У него может быть свой backend, свои sessions, партнерский order id и собственный analytics script. Если native app просто открывает:

https://partner-service.kz/start

то контекст почти гарантированно потеряется. Нужен 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...

Требования к handoff token:

  1. Short TTL: минуты, не дни.
  2. Audience-bound: только конкретный service id.
  3. Return path signed: WebView не должен сам решать, куда вернуть пользователя.
  4. No raw PII: token может указывать на server-side subject, но не раскрывать ИИН, телефон или номер счета.
  5. Event correlation: token содержит или позволяет получить event_id / ctx_id.

Когда WebView завершает действие, он должен вернуть событие в 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...'
}));

Но финальное measurement-событие лучше подтверждать 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

Иначе покупка может улететь в аналитику до того, как платеж реально подтвержден.

Как должны выглядеть ссылки для депозитов, платежей и сервисов

Плохая ссылка:

https://homebank.kz/

Она не addressable. Рекламная система не знает, какой продукт продвигался. Приложение не знает, куда вести пользователя. BI потом пытается угадать intent по campaign name.

Слабая ссылка:

https://homebank.kz/deposits

Она лучше, но не хватает категории, currency, offer id, route fallback и attribution context.

Нормальная ссылка:

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...

Для платежей:

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

Для WebView-сервиса:

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...

Для MMP OneLink можно хранить route в deep_link_value, а параметры - в 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

Но приложение все равно должно нормализовать это в собственный route:

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

Что делать с неустановленным приложением

Если app установлен, verified link должен открыть приложение. Если не установлен, ссылка должна открыть fallback: App Store, Google Play или web landing page. Здесь MMP обычно полезен, потому что умеет route-by-platform и deferred deep linking.

Но нельзя рассчитывать, что App Store или Play Store сохранят полный URL как есть. На Android есть Install Referrer API, через который можно получить referrer content из Google Play. На iOS прямой referrer через install flow принципиально ограничен, поэтому deferred deep link чаще держится на MMP attribution match и privacy-safe payload.

Правильная модель:

  1. Для installed users - App Links / Universal Links.
  2. Для not installed - MMP link или link gateway с store fallback.
  3. Для deferred route - deep_link_value + sub params, а не надежда на полный raw URL после install.
  4. Для paid attribution - MMP/ads integration, а не самодельная UTM-таблица.
  5. Для bank compliance - consent-aware storage and deletion of click context.

QA matrix: как понять, что deep links реально работают

Тестировать нужно не "ссылка открывает приложение", а весь route.

СценарийОжидаемый результат
Installed + logged inОткрывается точный продуктовый экран
Installed + logged outLogin, затем тот же продуктовый экран
Installed + KYC missingKYC flow, затем return_to или понятный 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 есть, ctx не потерян, service event возвращается
Expired WebView tokenRefresh/handoff retry, не home screen
Invalid provider_idControlled fallback to category screen
utm_* + MMP paramsContext ledger содержит оба слоя
Sensitive params in URLLink rejected or sanitized

Команды для технического QA:

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

Проверки:

  1. Status 200.
  2. No redirect for association files.
  3. Correct JSON body.
  4. Correct content type where 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 direct Safari address-bar navigation 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

В BigQuery нужно уметь ответить на вопросы:

  1. Какие кампании чаще всего теряют route после auth?
  2. Какие WebView-сервисы открываются, но не возвращают callback?
  3. Где больше всего invalid provider ids?
  4. Какие utm_campaign приводят к deeplink_received, но не к deeplink_opened?
  5. Сколько product purchases имеют ctx_id, а сколько выглядят как organic because context was dropped?

Что бы я заставил SuperApp-команду сделать за 30 дней

  1. Выбрать canonical link domain: например go.bank.kz или verified homebank.kz, но без смешивания с неассоциированными host variants.
  2. Прописать App Links / Universal Links для production, staging и dev раздельно.
  3. Убрать marketing links с доменов, где association endpoints возвращают HTML/404.
  4. Создать registry deep-link templates для deposits, payments, transfers, loans, cards, marketplace, services.
  5. Добавить ctx_id ledger и перестать носить чувствительный контекст в query string.
  6. Переделать login/KYC flow так, чтобы state=ctx_id переживал auth.
  7. Сделать WebView handoff token и return_to contract.
  8. Нормализовать MMP payload в тот же route contract.
  9. Логировать deeplink_received, deeplink_resolved, deeplink_opened, deeplink_failed.
  10. Собрать QA dashboard по route loss, auth loss, WebView callback loss и attribution loss.

Главная мысль

Если SuperApp не умеет открывать конкретный депозит, платеж, кредит или WebView-сервис по стабильной ссылке, маркетинг обречен оптимизировать не продукт, а абстрактный app open. Если cross-authorization теряет URL parameters, платная кампания может привести правильного пользователя, но событие покупки будет выглядеть как organic или generic app usage. Если MMP payload живет отдельно от native router, deferred deep linking будет случайностью.

Deep-linking в SuperApp нужно проектировать как инфраструктуру роста: verified hosts, template registry, context ledger, auth-safe return path, WebView handoff, MMP merge rules and measurement events. Тогда ссылка из рекламы, push, SMS или QR ведет не "в приложение", а в нужный продукт с сохраненным attribution context.

Sources