AppsFlyer (S2S)
AppsFlyer é a integração de atribuição mobile. O front dispara três eventos de funil (lead, dep, rebill) por dois caminhos distintos — bridge nativo do APK ou HTTP server-to-server — e o gate é UA mobile, não modo app.
:::caution Não é "só TWA"
Uma versão anterior desta página dizia que AppsFlyer "só funciona em modo TWA (?app=true)". Não é o caso: useAppsFlyer.ready = enabled && isMobileUA. Usuário de navegador mobile sem APK também dispara, pelo caminho HTTP. O que o modo app decide é qual dos dois caminhos é usado, não se o evento sai.
Desktop é filtrado nos dois caminhos (paridade com o filtro phone-only do legado). :::
Configuração por brand
// overrides/<brand>/app/config/analytics/appsflyer.ts
import type { AppsFlyerConfig } from "~/types/appsflyer";
export const appsFlyerConfig: AppsFlyerConfig = {
enabled: true,
s2sUrl: "https://appsflyer.gdm7khub.com/appsflyer",
afReferrer: "349825",
postMessageMinVersion: "3.0.0",
};
| Campo | Tipo | Pra quê serve |
|---|---|---|
enabled | boolean | Master switch, exigido pelos dois caminhos |
s2sUrl | string? | URL do proxy S2S da brand. Usada só pelo caminho HTTP. O proxy é quem lida com credenciais da API AppsFlyer |
afReferrer | string? | Referrer / agency id que identifica a brand no dashboard AppsFlyer |
brandSlug | string? | Slug enviado no header x-brand pro proxy S2S multibrand (ver abaixo) |
postMessageMinVersion | string? | Versão mínima de APK (semver) que expõe o bridge postMessage. Omitir força o caminho HTTP em todo evento |
:::danger Não existe appId
A versão anterior documentava appsFlyerConfig = { enabled, appId: "br.bet.k.twa" }. appId não existe em AppsFlyerConfig (app/types/appsflyer.ts) — não há campo de package name na config do front. O package name do APK é assunto do time mobile.
:::
Brands
| Brand | enabled |
|---|---|
7k-bet-br | ✅ true (dev e prod) — s2sUrl appsflyer.gdm7khub.com/appsflyer, afReferrer 349825, postMessageMinVersion 3.0.0 |
as outras 12 brands com override de appsflyer.ts | ❌ false |
Só a 7k tem AppsFlyer ativo hoje. As demais têm o arquivo criado com enabled: false, esperando s2sUrl + afReferrer da brand.
Modelo de funcionamento — dois caminhos
Path 1 — bridge nativo (postMessage)
Caminho dominante em produção. Ativo quando: está em modo app E o APK reporta versão >= postMessageMinVersion E window.twa existe. O front faz:
window.twa.getPostMessageService()
.postMessage(JSON.stringify({ event: "logAppsflyerEvent", ...payload }))
O APK então loga o evento via SDK AppsFlyer embarcado. Com o threshold da 7k em 3.0.0 e os APKs em produção historicamente acima disso, praticamente todo o tráfego TWA da 7k vai por aqui.
Em sucesso o front retorna — não dispara também o HTTP (é assim que se evita contagem dobrada). Em qualquer falha (bridge ausente, postMessage lança, service rejeita) ele cai pro Path 2.
Path 2 — HTTP S2S
Cobre: navegador mobile sem APK, APKs anteriores ao contrato de postMessage, e falha do bridge.
O front faz POST /api/tracking/appsflyer — uma rota interna do próprio front — que encaminha server-side pro s2sUrl da brand. A s2sUrl nunca aparece no client.
:::info Não existe endpoint /bff/.../afS2S
A versão anterior mostrava o evento indo do hook pro BFF num endpoint afS2S, e o BFF encaminhando pro AppsFlyer. Não é isso. O caminho HTTP passa pela rota /api/tracking/appsflyer do front (app/routes/api/tracking/appsflyer.ts), não pelo BFF.
:::
Requer s2sUrl e afReferrer configurados — sem eles o proxy responde 404, então a chamada é no-op. Brand que optou só pelo bridge pode deixar s2sUrl vazio.
Proxy multibrand (brandSlug)
A rota server-side tem dois contratos de upstream, escolhidos pela presença de brandSlug:
brandSlug | Headers enviados ao upstream |
|---|---|
| ausente (contrato legacy) | chamada simples pro s2sUrl dedicado da brand (ex: appsflyer.gdm7khub.com) |
| presente | x-brand (o slug) + x-api-key (do env APPSFLYER_API_KEY) + x-idempotency-key (UUID v4 por chamada) + x-request-timestamp (unix epoch em segundos) |
O contrato multibrand é o do proxy appsflyer.plataformizacao.net. APPSFLYER_API_KEY precisa estar provisionada no ambiente da brand — sem ela a rota loga erro e não chama.
Hoje nenhuma brand seta brandSlug no config de AppsFlyer — o caminho existe e está pronto, mas todas as chamadas seguem o contrato legacy.
Eventos enviados
| Método do hook | status no payload | Quando |
|---|---|---|
sendRegister() | lead | signup bem-sucedido (trackRegistration) |
sendFTD(payout) | dep | depósito confirmado com isFirstDeposit: true |
sendRebill(payout) | rebill | depósito confirmado que não é o primeiro |
Payload (AppsFlyerEventPayload):
{
"user_id": "12345",
"user_email": "...",
"phone": "...",
"first_name": "...",
"last_name": "...",
"city": "...",
"region": "...",
"country": "...",
"status": "dep",
"af_referrer": "349825",
"appsflyer_id": "<sessionStorage>",
"advertising_id": "<sessionStorage>",
"app_version": "<sessionStorage>",
"payout": 50.0
}
:::note Este payload carrega PII em claro
Diferente dos params de GTM (onde e-mail/telefone/nome vão hasheados em SHA-256), o payload de AppsFlyer envia user_email, phone, first_name, last_name, city, region em claro. No Path 2 isso sai do browser pra uma rota same-origin do front e daí server-side pro proxy. Registrado aqui como comportamento observado — o enquadramento de privacidade precisa da confirmação do responsável por LGPD antes de virar política documentada.
:::
appsflyer_id, advertising_id e app_version vêm de sessionStorage, populados dos query params da URL (af_id/appsflyer_id, aaid/advertising_id, app_version) que o APK injeta. A captura roda incondicionalmente, não gateada por modo app — porque o usuário pode chegar do APK numa URL com esses params e depois navegar num contexto de navegador mobile onde o Path 2 dispara. Sem esses IDs, o S2S funciona mas perde matching com a instalação original.
Por que S2S antes do gate de trackers?
Em useAnalytics, appsFlyer.sendRegister/sendFTD/sendRebill são chamados antes do if (!shouldLoadTrackers) return. Os dois escopos são quase inversos:
shouldLoadTrackers = !isAppMode // trackers web
useAppsFlyer.ready = enabled && isMobileUA
Se o S2S ficasse dentro do gate, no app (onde S2S é o único caminho válido) o early return o eliminaria. Foi um bug histórico real, corrigido em maio/2026: nem FTD nem rebill disparavam pra nenhum usuário.
:::warning A fórmula do gate mudou
shouldLoadTrackers é hoje só !isAppMode. A cláusula && !isBot foi removida junto com o branch de auditoria sintética na revisão de scheduler de 2026-06 — bots e Lighthouse carregam os mesmos scripts que usuário real, deliberadamente. Ver App Mode.
:::
Versão de APK gating
O gating não é um "2.5.0" hardcoded. É a config por brand postMessageMinVersion, comparada com o app_version de sessionStorage via versionGte:
if (isAppMode && minVersion && versionGte(appVersion, minVersion)) {
// Path 1 — bridge nativo
}
Valores de referência do legado, por brand: 7k "3.0.0". (Vera "5.0.0" e Cassino "4.0.20" estão documentados no tipo, mas essas brands saíram do base em 2026-07 — fora do escopo desta doc.)
Mantém compatibilidade retroativa com APKs velhos sem forçar update: APK abaixo do threshold simplesmente cai no Path 2.
Como debuggar
AppsFlyer dashboard
hq.appsflyer.com → seu app → Overview → eventos recebidos.
Em dev
?app=true (ou ?is_twa=true) na URL, mais os params de identidade, e injetar o bridge fake no console:
window.twa = {
getPostMessageService: async () => ({ postMessage: console.log }),
};
Sem o bridge, o fluxo cai no Path 2 e você vê o POST /api/tracking/appsflyer na Network.
Confira o sessionStorage: appsflyer_id, advertising_id, app_version.
Atenção: UA de desktop não dispara nada (ready = false). Use device emulation com UA mobile.
Anti-patterns
- Mover
appsFlyer.sendFTDpra dentro do gate de trackers. Quebra atribuição mobile (bug histórico de maio/2026). - Assumir que AppsFlyer só existe em TWA. Navegador mobile também dispara, via HTTP. Testar só dentro do APK esconde metade do fluxo.
- Usar
appIdna config. Não existe. Os campos sãoenabled,s2sUrl,afReferrer,brandSlug,postMessageMinVersion. - Setar
brandSlugsem provisionarAPPSFLYER_API_KEYno ambiente da brand. A rota loga erro e não chama o upstream. - Preencher
s2sUrlsemafReferrer(ou vice-versa). O Path 2 exige os dois; faltando um, é no-op silencioso. - Confiar em web desktop pra atribuição mobile. Sem
appsflyer_idválido não há matching com a instalação.