Pular para o conteúdo principal

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",
};
CampoTipoPra quê serve
enabledbooleanMaster switch, exigido pelos dois caminhos
s2sUrlstring?URL do proxy S2S da brand. Usada só pelo caminho HTTP. O proxy é quem lida com credenciais da API AppsFlyer
afReferrerstring?Referrer / agency id que identifica a brand no dashboard AppsFlyer
brandSlugstring?Slug enviado no header x-brand pro proxy S2S multibrand (ver abaixo)
postMessageMinVersionstring?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

Brandenabled
7k-bet-brtrue (dev e prod) — s2sUrl appsflyer.gdm7khub.com/appsflyer, afReferrer 349825, postMessageMinVersion 3.0.0
as outras 12 brands com override de appsflyer.tsfalse

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:

brandSlugHeaders enviados ao upstream
ausente (contrato legacy)chamada simples pro s2sUrl dedicado da brand (ex: appsflyer.gdm7khub.com)
presentex-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 hookstatus no payloadQuando
sendRegister()leadsignup bem-sucedido (trackRegistration)
sendFTD(payout)depdepósito confirmado com isFirstDeposit: true
sendRebill(payout)rebilldepó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 !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

  1. Mover appsFlyer.sendFTD pra dentro do gate de trackers. Quebra atribuição mobile (bug histórico de maio/2026).
  2. Assumir que AppsFlyer só existe em TWA. Navegador mobile também dispara, via HTTP. Testar só dentro do APK esconde metade do fluxo.
  3. Usar appId na config. Não existe. Os campos são enabled, s2sUrl, afReferrer, brandSlug, postMessageMinVersion.
  4. Setar brandSlug sem provisionar APPSFLYER_API_KEY no ambiente da brand. A rota loga erro e não chama o upstream.
  5. Preencher s2sUrl sem afReferrer (ou vice-versa). O Path 2 exige os dois; faltando um, é no-op silencioso.
  6. Confiar em web desktop pra atribuição mobile. Sem appsflyer_id válido não há matching com a instalação.