Инструкция по настройке AppsFlyer
Эта инструкция поможет настроить интеграцию мобильных приложений через AppsFlyer — платформу для мобильной атрибуции и маркетинговой аналитики.
Интеграция позволяет корректно учитывать заказы из мобильного приложения в CPA-канале:
-
Если у пользователя установлено мобильное приложение — мы направляем его в приложение, а наша система атрибуцирует все его покупки.
-
Если у пользователя не установлено мобильное приложение — он оформляет заказы на сайте по стандартной CPA-модели.
Рекламодатель платит не за установки, а за конкретные покупки. Мы приводим и атрибуцируем каждую покупку нужному партнёру и вебмастеру.
Содержание:
- Добавление приложения;
- Интеграция SDK AppsFlyer;
- Настройка отправляемых из приложения событий;
- Настройка панели администратора AppsFlyer;
- Настройка брендированных доменов;
- Проведение тестов.
Шаг 1. Добавление приложения
Если у вас еще нет аккаунта AppsFlyer, зарегистрируйте его здесь.
Перейдите на страницу добавления нового приложения и заполните необходимую информацию.
Подробнее: https://support.appsflyer.com/hc/en-us/articles/207377436-Adding-an-app-to-AppsFlyer
Шаг 2. Интеграция SDK AppsFlyer
Интегрируйте SDK AppsFlyer в ваше приложение, чтобы начать получать статистику о приложении и его пользователях.
Подробнее: https://support.appsflyer.com/hc/en-us/sections/6551164458257-Integrate-the-AppsFlyer-SDK
Шаг 3. Настройка отправляемых из приложения событий
Определите имя события для отправки. В нашем примере событием является тап по кнопке «Старт», поэтому мы назвали его "start_button_tap".
В реальном приложении события могут называться по-разному, например: "purchase", "level_up", "bet" и т.д.
Определив имя события, подготовьте массив (map/dictionary) withValues со следующими данными о событии:
- Сумма заказа/доход. В качестве ключа используется константа
AFInAppEventParameterName.REVENUE(Kotlin) илиAFEventParamRevenue(Swift), либо строкаaf_revenue(React Native). Опционально для Android. - ID заказа или события в системе рекламодателя. Ключ "order_id" или любой другой (по согласованию). Обязательно для передачи!
- Другие данные для атрибуции заказов: например, категория ("category"), тип клиента ("clientType") или промокод ("coupon"). Опционально.
Затем вызовите функцию logEvent() из AppsFlyerLib, передав ей имя события и массив с данными.
Реализация интеграции для iOS на этом не заканчивается – смотрите раздел Реализация на iOS (Swift, React Native).
Реализация на Kotlin
Пример реализации:
val eventData = HashMap<String, Any>()
eventData.put("orderId", orderId)
eventData.put(AFInAppEventParameterName.REVENUE, revenue)
eventData.put("category", category)
eventData.put("clientType", clientType)
eventData.put("coupon", coupon)
AppsFlyerLib.getInstance().logEvent(context, "start_button_tap", eventData)Реализация на React Native
Пример реализации:
appsFlyer.logEvent("purchase", {
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
});appsFlyer.logEvent("purchase", {
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
});Реализация на iOS (Swift, React Native)
Интеграция AppsFlyer на iOS сложнее из-за двух ограничений:
- Постбеки событий, отправленных с iOS, приходят с крайне ограниченным набором данных. Из нетрекинговых данных (отправляемых через
logEvent()) нам доступно только "af_revenue". Остальные нестандартные поля, включая "orderId", недоступны. - Постбек события установки приложения (первого запуска) не содержит AppsFlyerID — случайный ID, генерируемый SDK при первом запуске. Из-за этого покупки (или другие действия) невозможно связать с событием установки.
Оба ограничения можно обойти.
Отправка события покупки
Из нетрекинговых данных нам доступно только поле "af_revenue" — строковое поле, в котором AppsFlyer ожидает числовое значение.
Поле передаётся в постбеке без изменений, а AppsFlyer обрезает число до 5 знаков после запятой. Это позволяет закодировать в данное поле произвольные данные в виде числовой последовательности.
Для этого мы разработали специальный алгоритм — его реализацию смотрите в примерах ниже.
class NumSeqEncoder {
/// Encodes byte array into one large numeric string.
///
/// - Parameter bytes: Byte array with binary data.
/// - Returns: String with a long number.
static func encode(_ bytes: [UInt8]) -> String {
let size = bytes.count
var string: String = ""
string.reserveCapacity(Int(ceil(Double(size) * 8 / 3)))
var buffer: UInt32 = 0
var bufferSize = 0
var pos = 0
while pos < size {
let pickBytes = min(size - pos, bufferSize != 0 ? 3 : 4)
buffer = buffer << (pickBytes * 8)
for y in 0..<pickBytes {
buffer |= ((UInt32(bytes[pos + y]) & ((1 << 8) - 1)) << ((pickBytes - y - 1) * 8))
}
bufferSize += pickBytes * 8
pos += pickBytes
while bufferSize >= 3 {
if bufferSize >= 4 {
let quad = UInt(buffer >> (bufferSize - 4) & ((1 << 4) - 1))
if quad == 0b1000 || quad == 0b1001 {
string += String(quad)
bufferSize -= 4
buffer &= (1 << bufferSize) - 1
continue
}
}
let triple = UInt(buffer >> (bufferSize - 3) & ((1 << 3) - 1))
string += String(triple)
bufferSize -= 3
buffer &= (1 << bufferSize) - 1
}
}
if bufferSize > 0 {
string += String(buffer << (3 - bufferSize))
}
return string
}
/// Encodes order data into a string as a long float number.
///
/// - Parameters:
/// - data: Data to encode. Allowed up to 1441 bytes (not characters!).
/// - Returns: String with a long float number; up to 3851 characters/bytes.
static func encodeToRevenue(_ data: String) -> String {
let dataBytes = Array(data.utf8)
// 3852 is max known length for Appsflyer's revenue field (it's probably greater though).
if Int(ceil(Double(dataBytes.count) * 8 / 3)) > 3843 {
return "0.000000"
}
return "0.000000" + encode(dataBytes)
}
/// Decodes binary data from a string with a long number.
///
/// - Parameter string: String with a long number.
/// - Returns: Byte array with binary data.
static func decode(_ string: String) -> [UInt8] {
let bytes: [UInt8] = Array(string.utf8)
var data = [UInt8](repeating: 0, count: bytes.count * 4 / 8)
var bitIndex = 0
for i in 0..<bytes.count {
let code = UInt8(bytes[i])
if code < 48 || code > 57 {
return data
}
let number = code - 48
let len = number >= 8 ? 4 : 3
for y in stride(from: len - 1, through: 0, by: -1) {
let byteIndex: Int = bitIndex / 8
let byteBitIndex: Int = bitIndex % 8
data[byteIndex] |= (number >> y & 1) << (7 - byteBitIndex)
bitIndex += 1
}
}
let resultSize = Int((Double(bitIndex) / 8).rounded(.down)) - 1
return Array(data[0...resultSize])
}
/// Parses a string of data encoded as a long float number.
///
/// - Parameter string: String with a long float number.
/// - Returns: Data as String?.
static func decodeFromRevenue(_ string: String) -> String? {
let range = string.range(of: ".")
if range == nil {
return nil
}
let dataEncoded = String(string[string.index(range!.lowerBound, offsetBy: 7)...])
let dataDecoded = decode(dataEncoded)
return String(bytes: dataDecoded, encoding: .utf8)
}
}class NumSeqEncoder {
/**
* Encodes binary string into one large numeric string.
* @param {string} data String with binary data.
* @returns {string} String with a long number.
*/
static encode(data: string): string {
const bytes = (new TextEncoder().encode(data));
const size = bytes.length;
let string = "";
let buffer = 0;
let bufferSize = 0;
let pos = 0;
while (pos < size) {
let pickBytes = Math.min(size - pos, bufferSize != 0 ? 3 : 4);
buffer = buffer << (pickBytes * 8);
for (let i = 0; i < pickBytes; ++i) {
buffer |= (bytes[pos + i] & ((1 << 8) - 1)) << ((pickBytes - i - 1) * 8);
}
bufferSize += pickBytes * 8;
pos += pickBytes;
while (bufferSize >= 3) {
if (bufferSize >= 4) {
const quad = buffer >> (bufferSize - 4) & ((1 << 4) - 1);
if (quad === 0b1000 || quad === 0b1001) {
string = string.concat(quad.toString());
bufferSize -= 4;
buffer &= (1 << bufferSize) - 1;
continue;
}
}
const triple = buffer >> (bufferSize - 3) & ((1 << 3) - 1);
string = string.concat(triple.toString());
bufferSize -= 3;
buffer &= (1 << bufferSize) - 1;
}
}
if (bufferSize > 0) {
string = string.concat((buffer << (3 - bufferSize)).toString());
}
return string;
}
/**
* Encodes order data into a string as a long float number.
* @param {string} data String to encode. Allowed up to 1441 bytes (not characters!).
* @returns {string} String with a long float number; up to 3851 characters/bytes.
*/
static encodeToRevenue(data: string): string {
const size = new TextEncoder().encode(data).length;
// 3852 is max known length for Appsflyer's revenue field (it's probably greater though).
if (Math.ceil(size * 8 / 3) > 3843) {
return "0.000000";
}
return "0.000000" + NumSeqEncoder.encode(data);
}
/**
* Decodes binary data from a string with a long number.
* @param {string} string String with a long number.
* @returns {string} Decoded binary string.
*/
static decode(string: string): string {
const bytes = (new TextEncoder().encode(string));
const size = bytes.length;
let data = new Uint8Array(size * 4 / 8);
let bitIndex = 0;
for (let i = 0; i < size; ++i) {
if (bytes[i] < 48 || bytes[i] > 57) {
break;
}
const number = bytes[i] - 48;
let len = number >= 8 ? 4 : 3;
for (let y = len - 1; y >= 0; --y) {
const byteIndex = ~~(bitIndex / 8);
const byteBitIndex = bitIndex % 8;
data[byteIndex] = (data[byteIndex] || 0) | ((number >> y & 1) << (7 - byteBitIndex));
++bitIndex;
}
}
return new TextDecoder().decode(data.slice(0, Math.floor(bitIndex / 8)));
}
/**
* Parses a string of data encoded as a long float number.
* @param {string} string String with a long float number.
* @returns {string|null} Decoded binary string. Null if string is invalid.
*/
static decodeFromRevenue(string: string): string | null {
const pos = string.indexOf('.');
if (pos === -1) {
return null;
}
return NumSeqEncoder.decode(string.substring(pos + 7));
}
}"use strict";
class NumSeqEncoder {
/**
* Encodes binary string into one large numeric string.
* @param {string} data String with binary data.
* @returns {string} String with a long number.
*/
static encode(data) {
const bytes = (new TextEncoder().encode(data));
const size = bytes.length;
let string = "";
let buffer = 0;
let bufferSize = 0;
let pos = 0;
while (pos < size) {
let pickBytes = Math.min(size - pos, bufferSize != 0 ? 3 : 4);
buffer = buffer << (pickBytes * 8);
for (let i = 0; i < pickBytes; ++i) {
buffer |= (bytes[pos + i] & ((1 << 8) - 1)) << ((pickBytes - i - 1) * 8);
}
bufferSize += pickBytes * 8;
pos += pickBytes;
while (bufferSize >= 3) {
if (bufferSize >= 4) {
const quad = buffer >> (bufferSize - 4) & ((1 << 4) - 1);
if (quad === 0b1000 || quad === 0b1001) {
string = string.concat(quad.toString());
bufferSize -= 4;
buffer &= (1 << bufferSize) - 1;
continue;
}
}
const triple = buffer >> (bufferSize - 3) & ((1 << 3) - 1);
string = string.concat(triple.toString());
bufferSize -= 3;
buffer &= (1 << bufferSize) - 1;
}
}
if (bufferSize > 0) {
string = string.concat((buffer << (3 - bufferSize)).toString());
}
return string;
}
/**
* Encodes order data into a string as a long float number.
* @param {string} data String to encode. Allowed up to 1441 bytes (not characters!).
* @returns {string} String with a long float number; up to 3851 characters/bytes.
*/
static encodeToRevenue(data) {
const size = new TextEncoder().encode(data).length;
// 3852 is max known length for Appsflyer's revenue field (it's probably greater though).
if (Math.ceil(size * 8 / 3) > 3843) {
return "0.000000";
}
return "0.000000" + NumSeqEncoder.encode(data);
}
/**
* Decodes binary data from a string with a long number.
* @param {string} string String with a long number.
* @returns {string} Decoded binary string.
*/
static decode(string) {
const bytes = (new TextEncoder().encode(string));
const size = bytes.length;
let data = new Uint8Array(size * 4 / 8);
let bitIndex = 0;
for (let i = 0; i < size; ++i) {
if (bytes[i] < 48 || bytes[i] > 57) {
break;
}
const number = bytes[i] - 48;
let len = number >= 8 ? 4 : 3;
for (let y = len - 1; y >= 0; --y) {
const byteIndex = ~~(bitIndex / 8);
const byteBitIndex = bitIndex % 8;
data[byteIndex] = (data[byteIndex] || 0) | ((number >> y & 1) << (7 - byteBitIndex));
++bitIndex;
}
}
return new TextDecoder().decode(data.slice(0, Math.floor(bitIndex / 8)));
}
/**
* Parses a string of data encoded as a long float number.
* @param {string} string String with a long float number.
* @returns {string|null} Decoded binary string. Null if string is invalid.
*/
static decodeFromRevenue(string) {
const pos = string.indexOf('.');
if (pos === -1) {
return null;
}
return NumSeqEncoder.decode(string.substring(pos + 7));
}
}Необходимо отправлять два события: "purchase" и "advcake_purchase". Оба содержат данные о заказе, однако второе использует закодированное "revenue" вместо фактической суммы.
Включите в приложение класс NumSeqEncoder. При создании заказа пользователем сформируйте массив с данными о заказе и добавьте в него поле "appsflyerId" со значением функции AppsFlyerLib.shared().getAppsFlyerUID() (appsFlyer.getAppsFlyerUID()). Затем сформируйте такой же массив (или переиспользуйте существующий) и замените в нём поле AFEventParamRevenue (af_revenue) на результат функции NumSeqEncoder.encodeToRevenue(json), передав ей данные о заказе в виде JSON-строки.
Смотрите пример реализации ниже.
Размер JSON-строки с данными не должен превышать 1441 байт (не символов), что соответствует <=3851 байт в закодированном виде. Это ограничение AppsFlyer, выявленное в ходе тестирования. Вероятно, допустимы строки большего размера — информация будет уточнена, а инструкция и код обновлены.
Если переданная в функцию encodeToRevenue() строка превышает указанное ограничение, данные закодированы не будут (функция вернёт "0.000000").
Перед добавлением в JSON очередного поля убедитесь, что суммарная максимальная длина значений всех полей, ключей и токенов не превышает указанного ограничения.
Пример реализации:
struct OrderInfo: Codable {
let appsflyerId: String
let orderId: String
let revenue: Double
let category: String
let clientType: String
let coupon: String?
var dictionary: [String: Any?] {
return [
"appsflyerId": appsflyerId,
"orderId": orderId,
"revenue": revenue,
"category": category,
"clientType": clientType,
"coupon": coupon,
]
}
}
//...
let orderInfo = OrderInfo(
appsflyerId: AppsFlyerLib.shared().getAppsFlyerUID(),
orderId: orderId,
revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon
)
var afEventValues = orderInfo.dictionary
afEventValues[AFEventParamRevenue] = afEventValues["revenue"]
AppsFlyerLib.shared().logEvent("purchase", withValues: afEventValues)
let orderInfoJsonData = try JSONEncoder().encode(orderInfo)
let orderInfoJsonStr = String(data: orderInfoJsonData, encoding: .utf8) ?? ""
afEventValues[AFEventParamRevenue] = NumSeqEncoder.encodeToRevenue(orderInfoJsonStr)
AppsFlyerLib.shared().logEvent("advcake_purchase", withValues: afEventValues)const eventValues = {
appsflyerId: appsFlyer.getAppsFlyerUID(),
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
};
const eventValuesAdvcake = {...eventValues};
eventValuesAdvcake.af_revenue = NumSeqEncoder.encodeToRevenue(JSON.stringify(eventValues));
appsFlyer.logEvent("purchase", eventValues);
appsFlyer.logEvent("advcake_purchase", eventValuesAdvcake);const eventValues = {
appsflyerId: appsFlyer.getAppsFlyerUID(),
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
};
const eventValuesAdvcake = {...eventValues};
eventValuesAdvcake.af_revenue = NumSeqEncoder.encodeToRevenue(JSON.stringify(eventValues));
appsFlyer.logEvent("purchase", eventValues);
appsFlyer.logEvent("advcake_purchase", eventValuesAdvcake);Отправка дополнительного события установки
Дополнительное событие установки отправляется при первом запуске приложения, а также при переходе через Universal Links.
Это событие не влияет на статистику в AppsFlyer.
Swift
-
Включите в приложение класс
NumSeqEncoder, как описано в предыдущем разделе; -
Добавьте в приложение следующий код, который будет выполняться при каждом запуске на устройстве пользователя:
- Реализуйте метод
UlApplicationDelegate: application:continue: restorationHandler:. Этот метод вызывается при каждом открытии приложения через deeplink. Проверьте наличие параметраpid=advcake_intи соответствие хоста ссылки:
- Реализуйте метод
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
/// Если приложение открыто через диплинк и содержит параметр pid=advcake_int
let hosts = [
"yourapp.onelink.me"
]
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let params = components.queryItems,
let host = components.host,
hosts.contains(host),
params.first(where: { $0.name == "pid" })?.value == "advcake_int"
{
UserDefaults.standard.set(true, forKey: "advcakeDeeplink")
}
return true
}Вместо «yourapp» в yourapp.onelink.com укажите поддомен вашего OneLink-шаблона. Если у вас его ещё нет, мы создадим его в 5 шаге – Настройка панели администратора Appsflyer.
- Добавьте следующий код в функцию
applicationDidBecomeActive():
/// Если приложение не было установлено ранее, либо произошел переход через Universal Links - отправляем событие advcake_install
if !UserDefaults.standard.bool(forKey: "launchedBefore")
|| UserDefaults.standard.bool(forKey: "advcakeDeeplink")
{
let values = [
AFEventParamRevenue: NumSeqEncoder().enrichRevenue(0, AppsFlyerLib.shared().getAppsFlyerUID())
]
AppsFlyerLib.shared().logEvent("advcake_install", withValues: values as [AnyHashable: Any])
UserDefaults.standard.set(false, forKey: "advcakeDeeplink")
}
/// После необходимых действий, устанавливаем ключ true для lanuchedBefore
UserDefaults.standard.set(true, forKey: "launchedBefore")React Native
- Включите в приложение класс
NumSeqEncoder, как описано в предыдущем разделе; - Добавьте следующий код для выполнения при первом запуске на устройстве пользователя или при открытии через deeplink:
appsFlyer.initSdk({
devKey: 'K2***********99',
isDebug: false,
appId: '41*****44',
onInstallConversionDataListener: true, //Optional
onDeepLinkListener: true, //Optional
timeToWaitForATTUserAuthorization: 10 //for iOS 14.5
});
appsFlyer.logEvent("advcake_install", {
af_revenue: NumSeqEncoder.encodeToRevenue(appsFlyer.getAppsFlyerUID())
});appsFlyer.initSdk({
devKey: 'K2***********99',
isDebug: false,
appId: '41*****44',
onInstallConversionDataListener: true, //Optional
onDeepLinkListener: true, //Optional
timeToWaitForATTUserAuthorization: 10 //for iOS 14.5
});
appsFlyer.logEvent("advcake_install", {
af_revenue: NumSeqEncoder.encodeToRevenue(appsFlyer.getAppsFlyerUID())
});Шаг 4. Настройка панели администратора AppsFlyer
Включение ретаргетинга в настройках приложения
- Выберите ваше приложение и перейдите в раздел Configuration → App Settings;
- Выставите окно реатрибуции (рекомендуемое значение – 3 месяца);
- Установите галочку на «Re-engagement attribution»;
- Выставьте время между разными привязками (рекомендуемое значение – none).

Ретаргетинг включен. Перейдем к настройке OneLink-шаблона.
Настройка OneLink-шаблона
OneLink-шаблон нужен для того, чтобы пользователь при переходе по ссылке попадал сразу в приложение и при этом работал ретаргетинг.
- Если у пользователя установлено мобильное приложение — мы будем отправлять его в приложение, после чего наша система будет атрибуцировать все его покупки, 2. Если у пользователя не установлено мобильное приложение — он будет как прежде совершать заказы на сайте по CPA-модели.
- В панели администратора AppsFlyer перейдите в раздел Engage → Experiences & Deep Linking;
- Нажмите кнопку ⁝ и выберите пункт New OneLink template (или Edit OneLink template если шаблон уже имеется):

- Задайте произвольный поддомен:

Этот домен будет использоваться для создания ссылок.
-
Выберите поведение ссылки:
В разделе «When app isn't installed» (если приложение не установлено) укажите:
-
iOS: Redirect users to your domain: https://your-domain.ru/;
-
Android: Redirect users to your domain: https://your-domain.ru/;
Если у вас нет веб-версии, только приложение, используйте перенаправление в магазин:
-
iOS: Redirect users to AppStore;
-
Android: Redirect users to Google Play.
В разделе «When app is installed» (если приложение установлено) укажите:
-
iOS: Launching the app using Universal links;
-
Android: Launching the app using Android App links;
-
Android and iOS fallback: No fallback is set (стоит по умолчанию).
А также «Apple Team ID» (для iOS) и SHA256 (для Android) по кнопке ✎ (о том, что это такое и где найти, смотрите в подсказке ниже).
В разделе «When link is clicked on desktop» (если на ссылку кликнули на десктопе) укажите:
- Desktop: Redirect users to your domain: https://your-domain.ru/.
-
Вместо https://your-domain.ru/ укажите ссылку на свой основной лендинг.

Где найти «Apple Team ID» для iOS и SHA256 для Android?
- Apple Team ID: Перейдите на https://developer.apple.com/account/resources/certificates/list и авторизируйтесь под учетной записью разработчика – в верхней части страницы (там, где ваши Ф.И.) «Имя компании» и будет означать «Apple Team ID».
- SHA256: Перейдите на https://play.google.com/console/, затем, в меню слева, выберите «Настройка» → «Целостность приложения» → раздел «Сертификат ключа для подписи приложения». SHA256 будет указан в поле «Цифровой отпечаток сертификата SHA-256».
- Внесите правки в приложение:
Для Android
После заполнения SHA256 для Android вы получите возможность скопировать код, который необходимо добавить в AndroidManifest.xml:

Скопируйте код нажатием на кнопку и добавьте его в файл AndroidManifest.xml в блок, описывающий main activity.
Скопированный код можно расположить перед закрывающим тегом <activity>.
Ваш AndroidManifest.xml будет выглядеть так:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.advcake.getluckyapp">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GetLuckyApp">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GetLuckyApp.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Начало скопированного кода -->
<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"
android:host="subdomain.onelink.me"
android:pathPrefix="/ZBZ8" />
</intent-filter>
<!-- Конец скопированного кода -->
</activity>
</application>
</manifest>Для iOS
- Перейдите в Xcode;
- Нажмите на свой проект;
- Нажмите на таргет проекта;
- Перейдите на вкладку Capabilities;
- Активируйте переключатель Associated domains;
- Добавьте в список доменов ссылку, полученную после добавления поддомена. К ней нужно добавить префикс
applinks, после чего ссылка будет выглядеть так:applinks:subdomain.onelink.me:

- Вернитесь в файл
AppDelegate.swiftк методуUIApplicationDelegate:application:continue:restorationHandler:и замените поддомен yourapp в «yourapp.onelink.me» на ваш новый поддомен.
Сообщите нам название созданного OneLink-шаблона — это упростит тестирование с нашей стороны.
Шаблон и поведение ссылок настроены. Теперь добавим Adv.Cake как партнёра.
Добавление Adv.Cake
- В панели администратора AppsFlyer перейдите в раздел Collaborate → Active integrations:

- Введите «Adv.Cake» в строку поиска. Вы увидите двух партнёров — рекламную сеть и агентство. Необходимо предоставить доступ обоим:

-
Чтобы предоставить доступ для рекламной сети Adv.Cake:
3.1 Выберите партнёра с пометкой «Ad network»,
3.2 Нажмите Set up integration и на открывшейся вкладке Integration установите галочки «Activate partner» и «In-app event postbacks»:

3.3 Добавьте события из нашей сети: нажмите Add event и укажите нужное событие. В поле «Partner event identifier» введите название события (например, start_button_tapped), в «Send revenue» выберите values & revenue.
Обратите внимание, что в постбеках для iOS не будут приходить "values" – для этого, мы настраивали "revenue".

3.4 Перейдите на вкладку Attribution link, выберите шаблон в поле «Select OneLink template» (настраивали ранее) и активируйте галочку «Retargeting Settings» в нижней части страницы:

3.5 Перейдите на вкладку Permissions, установите галочки на указанных пунктах и нажмите Save Permissions в нижней части страницы:
4. Чтобы предоставить доступ для агентства Adv.Cake:
4.1 Выберите партнёра с пометкой «Agency»,
4.2 Нажмите Set up integration, перейдите на вкладку Permissions и установите галочки на указанных пунктах:

4.3 В нижней части страницы нажмите Save Permissions.
Шаг 5. Настройка брендированных доменов
Брендированные домены позволяют использовать в ссылках атрибуции название бренда и собственный домен.

Для чего нужны брендированные домены?
Многие блокировщики рекламы и VPN-сервисы блокируют ряд доменов, в том числе onelink.me. Пользователь не может перейти по ссылке и теряется. Брендированные домены решают эту проблему.
Брендированный домен связывается с доменом AppsFlyer через стандартные параметры DNS. Потребуется также внести небольшие правки в приложение и привязать домен в панели AppsFlyer.
Чтобы подключить брендированный домен, выполните следующие шаги:
Настройка DNS

- Выберите полное доменное имя, например
click.abcdef.com, гдеabcdef.com— название вашего бренда. Поддомен (например, click) можно настроить по своему усмотрению; - Попросите администратора DNS создать полный домен (хост);
- Попросите администратора DNS настроить запись CNAME, чтобы полный домен (домен бренда) указывал на нужный URL-адрес (хост AppsFlyer), как показано на схеме выше.
Привязка домена к AppsFlyer
- В панели администратора AppsFlyer перейдите в раздел Engage → Experiences & Deep Linking → Branded Domains;
- Нажмите Get started;
- В поле «Brand Domain» введите полный домен, как в записи DNS (например,
click.abcdef.com); - Выберите поддомен OneLink из открывшегося списка:

- Нажмите Verify.
Если диплинкинг выполняется с использованием брендированных доменов, SDK AppsFlyer не может получить данные о конверсиях по установкам и диплинкингу.
Настройка мобильного приложения
Используйте методы setOneLinkCustomDomain и oneLinkCustomDomains для Android и iOS соответственно.
Это позволяет отправлять запрос по брендированной ссылке, получать соответствующую ей ссылку OneLink и запрашивать данные о конверсиях.
Для Android
public class AFApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
AppsFlyerConversionListener conversionListener = new AppsFlyerConversionListener() {
}
AppsFlyerLib.getInstance().setOneLinkCustomDomain("promotion.greatapp.com");
AppsFlyerLib.getInstance().init(AF_DEV_KEY, conversionListener, this);
AppsFlyerLib.getInstance().start(this, AF_DEV_KEY);
}
}Если у вас несколько брендированных доменов, передайте их все в API:
AppsFlyerLib.getInstance().setOneLinkCustomDomain(
"promotion.greatapp.com",
"click.greatapp.com",
"deals.greatapp.com");Для iOS
Настройте брендированную ссылку для получения данных о конверсии:
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
AppsFlyerLib.shared().appsFlyerDevKey = "6CQi4Be6Zs9oNLsCusPbUL"
AppsFlyerLib.shared().appleAppID = "340954504"
AppsFlyerLib.shared().oneLinkCustomDomains = ["promotion.greatapp.com"]
//...
//...
}Если у вас несколько брендированных доменов, передайте их в API как массив строк:
AppsFlyerLib.shared().oneLinkCustomDomains = ["promotion.greatapp.com", "click.greatapp.com"]Теперь вшейте ссылку с брендированным доменом в приложение.
Для Android
- Откройте файл
AndroidManifest.xml; - Найдите блок
intent-filter, содержащий<action android:name=android.intent.action.VIEW />(если такого нет, скопируйте его из примера ниже и отредактируйте под свои ссылки); - Добавьте объект
<data />, описывающий ваш брендированный домен, например:<data android:scheme=https android:host=click.yourdomain.me />;
Пример файла AndroidManifest.xml после правок:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.advcake.getluckyapp">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GetLuckyApp">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GetLuckyApp.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<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"
android:host="subdomain.onelink.me"
android:pathPrefix="/ZBZ8" />
<!-- Начало добавленного кода -->
<data android:scheme="https"
android:host="click.your-domain.me" />
<!-- Конец добавленного кода -->
</intent-filter>
</activity>
</application>
</manifest>Для iOS
- Перейдите в Xcode;
- Нажмите на свой проект;
- Нажмите на таргет проекта (показано на скриншоте снизу);
- Перейдите на вкладку Capabilities;
- Активируйте переключатель Associated domains;
- Добавьте в список доменов ссылку с брендированным доменом с префиксом
applinks, например:applinks:click.your-domain.me:

- Вернитесь в файл
AppDelegate.swiftк методуUIApplicationDelegate:application:continue:restorationHandler:и добавьте ваш брендированный домен в массивhosts:
/// Если приложение открыто через диплинк и содержит параметр pid=advcake_int
let hosts = [
"yourapp.onelink.me",
"app.yourbrand.com", // Ваш брендированный домен
]
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let params = components.queryItems,
let host = components.host,
hosts.contains(host),
params.first(where: { $0.name == "pid" })?.value == "advcake_int"
{
UserDefaults.standard.set(true, forKey: "advcakeDeeplink")
}Шаг 6. Проведение тестов
После настройки поведения ссылок, добавления Adv.Cake как рекламной сети и настройки брендированных доменов создайте тестовую ссылку в панели администратора AppsFlyer.
- В панели администратора перейдите в раздел Collaborate → Active integrations → выберите партнёра Adv.Cake → Attribution link → Use OneLink;
- Выберите созданный ранее шаблон и скопируйте ссылку из поля «Click attribution link» в нижней части страницы.
Ссылка будет выглядеть так:
https://your-domain.onelink.me/BQt3?pid=advcake_int&c=affiliate&af_siteid=testweb&is_retargeting=true&af_reengagement_window=30d&af_click_lookback=7dНа данном этапе замените домен на брендированный. Ссылка из примера выше изменится на:
https://click.your-domain.me/BQt3?pid=advcake_int&c=affiliate&af_siteid=testweb&is_retargeting=true&af_reengagement_window=30d&af_click_lookback=7d- Лишние GET-параметры можно удалить, так как для теста они не понадобятся.
Подробнее о настройке брендированных доменов читайте в этом разделе.
- Перейдите по ссылке с ПК — должна открыться веб-версия вашего приложения. Если этого не произошло, проверьте настройки OneLink-шаблона.
- Отправьте ссылку на мобильные устройства для тестирования;
- Удалите приложение с мобильных устройств и перейдите по ссылке. Если открылась веб-версия приложения — всё настроено правильно. В противном случае перепроверьте настройки OneLink-шаблона;
- Установите приложение на мобильные устройства и перейдите по ссылке. Приложение должно перехватывать ссылки и открываться изнутри. Если этого не происходит — проверьте, все ли правки из предыдущего раздела внесены в приложение;
- Перейдите по новой ссылке и совершите тестовую покупку в приложении — должен выполниться ранее настроенный код.
Для просмотра событий перейдите в раздел Analyze → Events (обычно события появляются в течение часа).
Если события не появились — возможно, вы неверно настроили отправку событий из приложения. Обратите внимание на источник, за которым закрепилось событие:
- Если источник —
organic, ретаргетинг не настроен. - Если источник —
advcake_int, ретаргетинг настроен правильно.

Вам помогла эта страница?
Последнее изменение: 2026-01-23