Как мы узнаём покупателя
Вы разберёте, какими путями оплата в браузере доезжает до человека в вашем приложении и что происходит, когда путь не сработал.
Перед началом
- Ключ доступа проекта. Нужен для запросов с вашего сервера — где его взять, написано на странице «Аутентификация».
- ID проекта — для запросов с устройства. Кабинет: «Настройки проекта» → «Подключение приложения» → «Для разработчика».
- Имя пакета Android в разделе «Подключение приложения». Без него мы не соберём ссылку установки в Google Play, и покупателю останется письмо.
Что с чем связывается
Оплата в браузере ложится на идентификатор покупателя (guid), и приложение спрашивает право доступа по нему же. Поэтому у перехода одна задача — донести до приложения тот самый идентификатор, на который легла оплата.
Путей шесть, и они подстраховывают друг друга: промахнулся один — подхватывает следующий.
| Если у вас | Путь | Насколько надёжен |
|---|---|---|
| в приложении есть учётные записи | ваш идентификатор пользователя | не зависит от перехода вовсе |
| приложение уже установлено | код перехода в ссылке возврата | доезжает, если ссылка открыла приложение |
| Android, приложение ставят после оплаты | Install Referrer | доезжает при установке из Google Play |
| iOS, приложение ставят после оплаты, трекера нет | отпечаток устройства | совпадение не гарантировано |
| подключён AppsFlyer или Adjust | метка в диплинке трекера | надёжность самого трекера |
| ничего из перечисленного не сработало | письмо с кодом | работает всегда, но покупатель делает шаг руками |
Ваш идентификатор пользователя
Если человек заводит в приложении учётную запись, не ждите перехода вовсе: заведите покупателя у нас заранее и отправляйте на оплату уже готовый идентификатор.
curl -X POST "https://api.subster.ai/s2s/v1/users" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "externalId": "client-user-42", "email": "[email protected]" }'
# { "guid": "abc123def456", "user_id": "abc123def456",
# "paywallUrl": "https://pay.example.com/?pass=…&email=user%40example.com&origin=app" }
Нужно хотя бы одно из двух полей, иначе 400. user_id в ответе — тот же идентификатор. paywallUrl приходит, если в проекте вписан адрес страницы оплаты («Возврат в приложение», раздел «Продажа кнопкой внутри приложения»), иначе null.
Повтор с тем же externalId вернёт тот же идентификатор — храните эту пару у себя.
По одной только почте узнать человека нельзя: назвать чужой адрес может кто угодно. Пока почта не подтверждена, мы по ней не отдадим уже заведённый идентификатор, а заведём ещё одного покупателя. Подтверждённой почта становится, когда покупатель погасит код перехода из своей оплаты: перейдёт по ссылке возврата или введёт код руками.
Какой идентификатор победит в адресе экрана с ценами
Это про вас, если адрес экрана с ценами открываете вы сами — своим браузером в приложении или ссылкой в письме. Открываете нашей библиотекой — покупателем станет её идентификатор устройства, и право доступа ищите по нему.
Голого идентификатора в адресе нет, вместо него подписанный пропуск pass: он живёт 24 часа и меняется на идентификатор у нас на сервере. Метка origin=app в адресе уже стоит, а наши библиотеки дописывают к нему собственный идентификатор устройства. Правило разбора такое:
- метка и непустой идентификатор в адресе — покупателем считается он, даже когда рядом лежит пропуск;
- пропуск без идентификатора в адресе — покупателя даёт обмен пропуска;
- идентификатор без метки покупателем не считается.
Поэтому открывайте адрес как есть и не дописывайте свой идентификатор: им вы перебьёте пропуск и заберёте оплату на значение, о котором больше никто не спросит.
Код перехода в ссылке возврата
Приложение уже стоит на устройстве: после оплаты мы открываем его ссылкой возврата, и в ней лежит одноразовый код перехода. С библиотекой код меняется её вызовом — шаг 3 той же страницы. Без библиотеки — запросом:
GET /public/handoff/resolve?code=<код>
→ { "success": true, "data": { "guid": "abc123def456", "projectId": "…" } }
Ответ обёрнут: идентификатор лежит в data.guid, на верхнем уровне его нет. Обмен принимает до десяти запросов в минуту с одного IP-адреса, дальше 429.
Код живёт 72 часа и гаснет при первом успешном обмене. Сохраните идентификатор сразу: повторный запрос с тем же кодом отдаёт 404. Тем же ответом мы отвечаем на просроченный и на несуществующий код.
Код перехода и токен установки — разные значения одного вида: восемь знаков, одноразовые, меняются этим же запросом. Код приходит в ссылке возврата, на экране после оплаты и в письме и живёт 72 часа. Токен установки приносят Install Referrer и диплинк трекера, он живёт 30 дней.
Отказы открытых адресов этой страницы приходят без машинного кода (исключение — поле bundleId ниже), телом вида { "statusCode": 404, "message": "…" }: ветвитесь по статусу ответа. Потолки всех адресов сведены в «Аутентификации».
Install Referrer на Android
Путь, которого нет на iOS: токен перехода доносит до приложения сам Google Play. С нашей библиотекой достаточно вызвать Web2AppSdk.identify() при запуске — значение она прочитает и обменяет сама.
Кнопка на экране после оплаты ведёт в магазин по нашей ссылке …/store/apps/details?id=<пакет>&referrer=<токен>. Google Play сохраняет значение и отдаёт его установленному приложению. Токен установки живёт 30 дней — клик и установка могут разойтись на недели.
Если не сработал. Приложение поставили мимо Google Play — вручную из пакета, из другого магазина, на устройстве без сервисов Google — значение до приложения не доедет. Библиотека тогда попробует отпечаток устройства, а при промахе позовёт onNeedEmail: показывайте поле почты — это последний путь из таблицы.
Без библиотеки: Install Referrer
Прочитайте значение сами — библиотекой Google Play Install Referrer — и обменяйте его тем же запросом, что код перехода. Google Play отдаёт ровно то, что стояло в нашей ссылке, то есть сам токен.
// build.gradle: implementation("com.android.installreferrer:installreferrer:<version>")
val client = InstallReferrerClient.newBuilder(context).build()
client.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
val token = client.installReferrer.installReferrer
// exchange: GET https://api.subster.ai/public/handoff/resolve?code=<token>
// the buyer id is in data.guid of the response
}
client.endConnection()
}
override fun onInstallReferrerServiceDisconnected() {}
})
Обмен гасит токен так же, как код перехода: повторный запрос с тем же значением получит 404. Тот же отказ на первом же обмене значит, что приложение поставили не по нашей ссылке или токен истёк, — показывайте поле почты.
Отпечаток устройства
Путь для iOS, когда приложение ставят после оплаты, а трекера в проекте нет: ссылка возврата установку не переживает, аналога Install Referrer у iOS нет. На Android библиотека берёт этот путь, когда не сработал Install Referrer.
Страница после оплаты оставляет у нас отпечаток устройства: IP-адрес и заголовок браузера мы берём на сервере, разрешение экрана, часовой пояс и язык — из браузера. Приложение при первом запуске присылает свои признаки и получает идентификатор, если совпадение уверенное и единственное.
С библиотекой (с версии 0.7.0) это делает вызов опознания без кода: на iOS — Web2App.identify(deepLinkValue: nil), на Android — тот же вызов, что для Install Referrer.
Правила жёсткие:
- точное совпадение ищется два часа после оплаты, менее точное, по IP-адресу, — тридцать минут;
- под признаки подошли две оплаты, и язык устройства их не различает — отказ обеим;
- кандидат с противоречащим признаком не подойдёт даже при совпавшем IP-адресе.
Рекламные идентификаторы и разрешение на отслеживание не нужны: ни того, ни другого мы не запрашиваем.
Если не сработал. Общий Wi-Fi, приватный ретранслятор Apple или iPad с «настольным» браузером дают промах. Ответ на промах — тот же 404, что и на выключенный механизм: по нему нельзя понять, был ли кандидат. Библиотека сообщает об этом отдельно — .needsEmailFallback на iOS, тот же сигнал «нужна почта» на Android, — и дальше остаётся письмо.
Как выключить. «Настройки проекта» → «Подключение приложения» → вкладка «Основное» → ветка «Как приложение узнает вашего покупателя» → вариант «Не узнавать по устройству». Отпечаток тогда не собирается вовсе. Вариант «Через рекламный трекер» опознание по устройству не выключает.
Без библиотеки: отпечаток
Шлите запрос с самого устройства: IP-адрес — часть отпечатка, и мы берём его из соединения. Запрос через ваш сервер не совпадёт никогда. Лимит — десять запросов в минуту с одного IP-адреса.
curl -X POST "https://api.subster.ai/public/handoff/resolve-by-fingerprint" \
-H "Content-Type: application/json" \
-d '{"projectId":"<PROJECT_ID>","platform":"ios","osVersion":"18.6",
"screen":"393x852","timezone":"Europe/Berlin","language":"de-DE"}'
| Поле | Что передать |
|---|---|
projectId | ID проекта; обязательно |
platform | ios или android; обязательно |
osVersion | версия системы; обязательно, но в сверке не участвует |
screen | размер экрана в логических точках, как его видит браузер: на iOS — UIScreen.main.bounds, не пиксели. Ориентация не важна; обязательно |
timezone | часовой пояс в формате IANA, на iOS — TimeZone.current.identifier. Формально необязателен, но без него остаётся только менее точное совпадение по IP-адресу в первые тридцать минут |
language | язык устройства, de-DE, на iOS — Locale.preferredLanguages.first; разводит двух кандидатов с одного адреса |
bundleId | необязательно: Bundle ID без Team ID или имя пакета. Если пришлёте, а в кабинете значение этой платформы вписано, они должны совпасть — иначе 404. Недопустимые знаки — 400 с кодом VALIDATION_BUNDLE_ID_FORMAT |
{ "success": true, "data": { "guid": "abc123def456", "matchMethod": "fingerprint" } }
Значений у matchMethod два: fingerprint — точное совпадение, ip — менее точное, по IP-адресу. Отпечаток одноразовый: удачное опознание гасит его, поэтому идентификатор сохраните сразу.
Метка в диплинке трекера
У вас подключён AppsFlyer или Adjust — токен перехода поедет прямо в диплинке трекера. Ссылку установки мы соберём на сервере из ссылки трекера, указанной в кабинете.
Место в кабинете: «Настройки проекта» → «Подключение приложения» → вкладка «Основное» → ветка «Как приложение узнает вашего покупателя» → вариант «Через рекламный трекер». Там два поля: «Какой у вас трекер» и «Ссылка вашего приложения внутри трекера».
| Трекер | Где лежит токен | Ещё поля |
|---|---|---|
| AppsFlyer | deep_link_sub1, дубль в af_sub1 | в deep_link_value всегда слово handoff, а не токен; deep_link_sub2 — ID экрана с ценами, deep_link_sub3 — ID проекта, deep_link_sub4 — куда вела кнопка: app или store |
| Adjust | user_id, дубль в adj_label | у классической ссылки app.adjust.com параметры ещё и упакованы в deep_link=, если у проекта есть схема возврата: декодируйте значение как адрес <схема>://handoff?user_id=<токен>&… и возьмите user_id из его параметров |
Токен меняется так же, как код перехода: библиотекой — вызовом Web2App.identify(deepLinkValue:) на iOS или Web2AppSdk.identifyWithDeepLinkValue на Android, без неё — запросом GET /public/handoff/resolve?code=<токен>.
Если не сработал. У ссылок Adjust вида *.go.link и *.adj.st обязателен собственный параметр трекера adj_t: без него переход отвечает 404, и починить это с нашей стороны нельзя — копируйте ссылку из кабинета Adjust целиком. Для iOS ссылка установки собирается только при настроенном трекере; без трекера остаются отпечаток устройства и письмо.
Письмо с кодом
Последний рубеж, когда всё остальное промахнулось: код перехода утрачен, устройство сменилось, приложение переустановили. Приложение показывает поле почты и отправляет адрес: библиотекой — вызовом requestEmailRecovery(email), без неё — запросом.
curl -X POST "https://api.subster.ai/public/handoff/email-recovery/request" \
-H "Content-Type: application/json" \
-d '{ "projectId": "<PROJECT_ID>", "email": "[email protected]" }'
Ответ всегда 204 — по нему нельзя понять, знаком ли нам этот адрес. Если адрес найден, на него уходит письмо со ссылкой возврата и кодом перехода; дальше путь тот же, что выше.
Два ограничения работают одновременно: пять запросов за пятнадцать минут с одного IP-адреса, дальше 429, и двадцать минут между письмами на одну почту. Второе не видно в ответе: приложение получит 204, а письмо повторно не уйдёт. Покажите покупателю, что письмо придёт не мгновенно, и не предлагайте повтор, пока не пройдут двадцать минут.
Тот же код покупатель видит на экране после оплаты, с подписью «Или введите код вручную». Поле для него в приложении делаете вы: уберите пробелы, приведите введённое к заглавным буквам и отдайте в тот же обмен, что код из ссылки. Код сравнивается буква в букву, и набранный строчными не найдётся.
Идентификатор принадлежит одной организации
Идентификатор закрепляется за проектом, в котором появился впервые. Оплатить с ним можно в любом проекте вашей организации — например, если воронки разведены по платформам или по странам.
Идентификатор из проекта другой организации отклоняется до денег: 400 с кодом VISITOR_ID_FOREIGN_PROJECT в поле code, рядом причина для разработчика. Ветвитесь на код, а не на текст.
Дальше
- Проверить право доступа — что спросить по готовому идентификатору и как разобрать ответ.
- Возврат в приложение — настроить ссылки, которыми браузер открывает приложение.
- SDK — какие вызовы делают всё перечисленное за вас на iOS и Android.
- Диагностика — что смотреть, когда покупатель оплатил, а доступа в приложении нет.