S Subster

Как мы узнаём покупателя

Вы разберёте, какими путями оплата в браузере доезжает до человека в вашем приложении и что происходит, когда путь не сработал.

Перед началом

Что с чем связывается

Оплата в браузере ложится на идентификатор покупателя (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.

Правила жёсткие:

Рекламные идентификаторы и разрешение на отслеживание не нужны: ни того, ни другого мы не запрашиваем.

Если не сработал. Общий 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"}'
ПолеЧто передать
projectIdID проекта; обязательно
platformios или 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 — токен перехода поедет прямо в диплинке трекера. Ссылку установки мы соберём на сервере из ссылки трекера, указанной в кабинете.

Место в кабинете: «Настройки проекта» → «Подключение приложения» → вкладка «Основное» → ветка «Как приложение узнает вашего покупателя» → вариант «Через рекламный трекер». Там два поля: «Какой у вас трекер» и «Ссылка вашего приложения внутри трекера».

ТрекерГде лежит токенЕщё поля
AppsFlyerdeep_link_sub1, дубль в af_sub1в deep_link_value всегда слово handoff, а не токен; deep_link_sub2 — ID экрана с ценами, deep_link_sub3 — ID проекта, deep_link_sub4 — куда вела кнопка: app или store
Adjustuser_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, рядом причина для разработчика. Ветвитесь на код, а не на текст.

Дальше