Visão geral
Uma mensagem in-app é um card ou uma barra que um SDK exibe dentro do app, enquanto ele está aberto, em um de quatro layouts. O painel (ou a API REST de administração) cria a mensagem e sua segmentação; o SDK apenas pergunta ao servidor o que pode exibir e reporta o que aconteceu.
A entrega é dirigida pelo servidor e a renderização é feita no cliente:
- O servidor decide a elegibilidade (agendamento, plataforma, segmento e frequência). O SDK nunca avalia regras de segmentação por conta própria.
- O SDK renderiza o payload retornado com componentes nativos e reporta eventos de engajamento. O servidor os contabiliza.
Esta página é o contrato entre o servidor e qualquer SDK (React Native, iOS, Android ou uma integração própria). As duas rotas de dispositivo estão na Referência da API In App; esta página acrescenta as regras que a referência não consegue expressar.
As duas rotas autenticam com a chave de cliente do App (Authorization: Bearer ck_..., a mesma chave usada no registro de dispositivo) e comprovam a posse do dispositivo com o push token do dispositivo no corpo da requisição. Uma chave REST é rejeitada com 403, e uma chave ausente ou inválida com 401.
Fluxo da sessão
- Início da sessão. Quando o SDK estiver inicializado e o dispositivo registrado (ou seja, houver um
device_id), chameeligible. Não chame a cada tela: a rota tem limite de requisições por dispositivo (veja Limites). - Decida o que exibir.
eligibleretorna até 5 mensagens com gatilhosession_starte até 10 com gatilhoevent, da mais antiga para a mais nova dentro de cada grupo (veja Gatilhos).- Exiba imediatamente a primeira mensagem de início de sessão que o SDK conseguir renderizar.
- Mantenha as mensagens de evento em memória e exiba-as quando o app reportar o evento correspondente.
- No máximo uma mensagem fica visível por vez, e cada mensagem é exibida no máximo uma vez por sessão.
- Reporte a impressão uma vez por exibição, não por renderização. Envie uma
impressionquando a mensagem ficar visível para o usuário. Uma nova renderização, uma rotação ou um novo layout da mesma exibição não deve enviar outra. Envie a impressão antes de qualquerclickoudismissdessa exibição. - Reporte a interação.
- O toque em um botão envia
clickcom obutton_iddesse botão. - O toque na imagem, quando a mensagem tem
image_action, executa essa ação e enviaclickcombutton_id: "image". - Em uma mensagem
topoubottom, o toque no corpo executa a ação do primeiro botão e reporta umclickcom o id desse botão. - Fechar a mensagem envia
dismisscom umreason:close_button,swipeououtside_tapquando o usuário a fechou,autoquando o temporizador a fechou. Um dismiss só faz sentido após uma impressão. - Quando um clique fecha a mensagem, reporte o clique; um dismiss reportado depois de um clique é ignorado pela taxa de dispensa.
- O toque em um botão envia
Buscando mensagens elegíveis
POST /v1/apps/{app_id}/in-app-messages/eligible
Authorization: Bearer ck_example
Content-Type: application/json{
"device_id": "00000000-0000-4000-8000-000000000001",
"token": "example-push-token"
}Os dois campos são obrigatórios. Campos desconhecidos são rejeitados com 422 invalid_body.
Resposta 200:
{
"messages": [
{
"id": "00000000-0000-4000-8000-0000000000a1",
"format": "center",
"title": "Example title",
"body": "Example body text.",
"image_url": "https://cdn.example.com/example.png",
"image_action": { "type": "url", "value": "https://example.com/offer" },
"style": {
"background_color": "#102030",
"text_color": "#FFFFFF",
"button_color": "#E94B8B",
"button_text_color": "#FFFFFF",
"text_align": "center",
"margin": true
},
"dismiss_after_seconds": 8,
"trigger": { "type": "event", "name": "checkout_done" },
"buttons": [
{
"id": "a1b2c3d4",
"label": "See more",
"action": { "type": "deeplink", "value": "exampleapp://offers/42" }
},
{
"id": "e5f6a7b8",
"label": "Docs",
"action": { "type": "url", "value": "https://example.com/docs" }
}
]
}
]
}messages é sempre um array; fica vazio quando nada é elegível, e também quando o App está desativado.
Payload
| Campo | Tipo | Notas |
|---|---|---|
id | string (UUID) | Id da mensagem; vai no path da rota events. |
format | top, center, bottom ou full | Define o layout. Um SDK que não conhece um formato renderiza center. |
title | string | Até 100 caracteres. Pode estar vazio quando body está preenchido. |
body | string | Até 500 caracteres, pode conter quebras de linha. Pode estar vazio quando title está preenchido. |
image_url | string | Ausente quando a mensagem não tem imagem. Sempre https. |
image_action | object | Ausente quando a imagem não é tocável. Mesmo formato da ação de um botão: type (url ou deeplink) e value. Veja Ação da imagem. |
style | object ou null | null significa os padrões do SDK. Veja Estilo. |
dismiss_after_seconds | integer ou null | null mantém a mensagem até o usuário fechá-la. Veja Fechamento automático. |
trigger | object | Sempre presente. {"type": "session_start"} ou {"type": "event", "name": "..."}. Veja Gatilhos. |
buttons | array | De 0 a 2 botões, na ordem de exibição. |
buttons[].id | string | Opaco, atribuído pelo servidor e estável entre edições da mensagem. Envie-o de volta como button_id. |
buttons[].label | string | Até 30 caracteres. |
buttons[].action.type | url, deeplink ou push_permission | url é sempre https; deeplink é https ou um scheme customizado do app. push_permission é uma ação integrada do SDK, veja Ação de permissão de push. |
buttons[].action.value | string | O link a abrir. Ausente em push_permission: a chave não é enviada, nem como null. |
A segmentação (segmento, plataformas, agendamento, frequência) nunca faz parte do payload.
Layouts
A tabela de layouts é o contrato. Os tamanhos são limites, não especificações em pixels: cada plataforma renderiza com seus próprios componentes, mas deve ficar dentro dos limites e oferecer os gestos de fechamento listados aqui.
format | Posicionamento | Fundo (backdrop) | Gestos de fechamento | Imagem |
|---|---|---|---|---|
top | Barra abaixo da área segura superior, no máximo 40% da altura da tela | nenhum | close_button, swipe para cima | miniatura pequena à esquerda |
bottom | Barra acima da área segura inferior, no máximo 40% da altura da tela | nenhum | close_button, swipe para baixo | miniatura pequena à esquerda |
center | Card centralizado, no máximo 90% da largura | 50% preto | close_button, outside_tap | acima do texto |
full | De ponta a ponta dentro das áreas seguras, botões fixos na parte inferior | não se aplica | somente close_button | acima do texto |
- Um controle de fechar é sempre exibido, qualquer que seja o layout e o temporizador.
style.margin: trueaplica uma margem de 16 pontos às bordas da tela (top,bottomefull);falseremove a margem.- Com dois botões,
topebottomos posicionam lado a lado e os quebram em coluna quando não cabem. - Em uma mensagem
topoubottom, o toque no corpo executa a ação do primeiro botão e reporta umclickcom o id desse botão. - Todo fechamento reporta o
reasondo gesto que foi usado.
Estilo
style traz tokens opcionais de aparência. Um token ausente (ou style: null) significa o padrão do SDK.
| Token | Valores | Aplica-se a |
|---|---|---|
background_color | #RRGGBB | O fundo do card ou da barra. |
text_color | #RRGGBB | Título e corpo. |
button_color | #RRGGBB | Fundo do botão. |
button_text_color | #RRGGBB | Rótulo do botão. |
text_align | left, center ou right | Título e corpo. Ausente significa left. |
margin | boolean | Veja Layouts. Ausente significa true. |
Quando button_color ou button_text_color está definido, todos os botões o usam (o que estiver ausente mantém o padrão do SDK), de modo que o botão secundário deixa de ser desenhado com um estilo diferente.
As cores são #RRGGBB sem alpha, em maiúsculas, para que as duas plataformas renderizem o mesmo valor. O SDK deve ignorar uma cor que não consiga interpretar e usar o padrão daquele token em vez de falhar. O servidor não verifica contraste; o painel apenas avisa o autor.
Ação da imagem
Quando image_action está presente, o toque na imagem a executa e reporta click com button_id: "image". Sem image_action, a imagem não é tocável. A ação segue as mesmas regras e verificações de link da ação de um botão (veja Texto simples e links).
Fechamento automático
Quando dismiss_after_seconds está definido (de 1 a 300), o SDK fecha a mensagem essa quantidade de segundos depois de ela ter ficado visível e reporta dismiss com reason: "auto". A contagem regressiva pausa enquanto o app está em segundo plano. O controle de fechar continua disponível durante a contagem, e o usuário ainda pode fechar a mensagem antes, o que reporta o reason do próprio gesto.
Gatilhos
A busca continua no início da sessão: uma chamada a eligible por sessão.
{"type": "session_start"}: exibir a mensagem no início da sessão (a primeira renderizável, veja Fluxo da sessão).{"type": "event", "name": "checkout_done"}: manter a mensagem em memória e exibi-la quando o app reportar um evento com esse nome exato por meio dotrackEvent(name)do SDK. O SDK exibe a mensagem retida mais antiga cujo gatilho nomeia o evento, somente quando nenhuma mensagem está visível, e cada mensagem no máximo uma vez por sessão.
Os nomes de evento são comparados localmente: o SDK nunca os envia ao servidor e descarta as mensagens retidas quando a sessão termina. O servidor não mantém um registro de nomes de evento; o painel sugere os nomes já usados pelas mensagens do App.
A elegibilidade (público, plataforma, agendamento e frequência) é decidida pelo servidor no momento da busca para os dois tipos, e a impressão é reportada quando a mensagem é de fato exibida.
Compatibilidade
- O SDK deve ignorar campos do payload que não conhece.
- Para um
formatdesconhecido, renderizecenter. - Mudanças neste payload após a primeira versão do SDK são somente aditivas.
Ação de permissão de push
Um botão pode carregar {"type": "push_permission"}, sem value. É a única ação que o próprio SDK executa, e ela não recebe dados do autor da mensagem: um SDK continua sem nunca abrir um link fornecido pelo autor, apenas entrega as ações url e deeplink ao app. Somente botões a aceitam; image_action continua sendo url ou deeplink.
Ao toque, reporte primeiro o click, feche a mensagem e depois aja sobre a permissão de notificação do sistema operacional conforme ela estiver naquele momento:
| Estado | O que o SDK faz |
|---|---|
| Não decidido | Exibe o prompt de permissão do sistema. |
| Negado | Abre as configurações de notificação do app. Não exibe um prompt que o sistema operacional vai suprimir. |
Já concedido (mudou após eligible) | Nada. |
| Desconhecido | Nada, e não registra em log nada sensível. |
No Android abaixo do 13 não há prompt: um dispositivo com as notificações desativadas vai para as configurações. No Android 13 e superior o sistema operacional não distingue "nunca perguntado" de "negado permanentemente", então o SDK lembra que já exibiu o prompt uma vez e abre as configurações nos toques seguintes. Um usuário que negou em definitivo em uma instalação anterior ou por outro caminho pode não ver nada no primeiro toque.
O click listener do app recebe o clique com action: null, e a ação é executada com ou sem listener. A ação não acrescenta dados ao que o SDK já coleta ou envia: permission_status já faz parte do perfil do dispositivo, e o diálogo do sistema é uma escolha do próprio usuário.
Texto simples e links
- Renderize
titleebodycomo texto simples. Nunca os interprete como HTML ou Markdown, e nunca os passe a uma web view como marcação. - O servidor rejeita um link cujo scheme seja
http,javascript,data,file,vbscript,blobouabout. Um link aceito é uma URLhttpscom host ou (paradeeplink) um scheme customizado do app, comoexampleapp://offers/42. Mesmo assim, o SDK deve abrir apenas links que espera, e umdeeplinkque o app não consiga tratar deve ser ignorado em vez de causar um crash.
Regras de elegibilidade
O servidor retorna uma mensagem somente quando todas estas condições valem no momento da requisição:
- A mensagem está ativa, não foi excluída e está dentro da sua janela:
starts_atjá passou (quando definido) eends_atnão passou (quando definido). - A mensagem segmenta a plataforma do dispositivo (uma lista de plataformas vazia segmenta todas).
- O dispositivo pertence a pelo menos um dos segmentos incluídos da mensagem (nenhum segmento incluído segmenta todos os dispositivos) e a nenhum dos segmentos excluídos. A permissão de push não é exigida: o In-App não depende dela.
- A frequência permite outra exibição (abaixo).
- Uma mensagem com um botão
push_permissioné exibida somente a um dispositivo que possa usá-lo. Ela fica oculta quando o status de push do dispositivo éok, e quando osdk_versiondo dispositivo está vazio, ilegível ou é mais antigo que o mínimo do SDK que o dispositivo informa emsdk_name. Os mínimos são React Native0.8.0e iOS0.1.0; nenhum outro SDK recebe a mensagem ainda. Um dispositivo que não informasdk_nameé tratado como React Native, então precisa da0.8.0ou mais recente. Um dispositivo com status de pushoffouinvalidcontinua elegível. Esta regra falha de forma fechada: um SDK,sdk_nameou versão desconhecidos nunca recebem a mensagem.
Edições, ativação e pausa chegam aos dispositivos na próxima chamada a eligible, com um atraso de até cerca de 60 segundos. Edições nas regras de um segmento usado por uma mensagem podem levar até 60 segundos para serem vistas pela elegibilidade.
Frequência
O servidor conta os eventos impression do próprio dispositivo para aquela mensagem. Cliques e fechamentos não contam.
| Tipo | A mensagem é elegível enquanto |
|---|---|
once | o dispositivo não tem nenhuma impressão dela. |
n_times | o dispositivo tem menos de max impressões dela. |
interval | o dispositivo não tem nenhuma impressão dela, ou a última tem pelo menos interval_seconds de idade. |
Como o servidor é a autoridade, um SDK não deve manter seu próprio estado de "já exibida" para suprimir mensagens, e deve reportar as impressões de forma confiável: uma impressão perdida torna uma mensagem once elegível novamente.
Reportando eventos
POST /v1/apps/{app_id}/in-app-messages/{id}/events
Authorization: Bearer ck_example
Content-Type: application/json{
"device_id": "00000000-0000-4000-8000-000000000001",
"token": "example-push-token",
"event_id": "00000000-0000-4000-8000-0000000000e1",
"type": "click",
"button_id": "a1b2c3d4",
"occurred_at": "2026-10-17T14:30:05Z"
}| Campo | Obrigatório | Regras |
|---|---|---|
device_id, token | sim | Iguais aos de eligible. |
event_id | sim | Um UUID gerado pelo cliente uma vez por evento. Reutilize o mesmo valor ao tentar enviar esse evento novamente. |
type | sim | impression, click ou dismiss. |
button_id | para click | O id de um dos botões da mensagem, ou image para um toque na imagem de uma mensagem que tem image_action. Não permitido nos outros tipos. |
reason | para dismiss | close_button, swipe ou outside_tap quando o usuário fechou a mensagem, auto quando foi o temporizador. Não permitido nos outros tipos. |
occurred_at | não | RFC3339. Usado somente quando está dentro de 7 dias no passado e 5 minutos no futuro em relação ao horário do servidor; caso contrário, o servidor grava o próprio horário. |
Campos desconhecidos são rejeitados com 422 invalid_body.
Idempotência e novas tentativas
Os eventos são deduplicados por (message, device, event_id):
- Primeira entrega:
201com{ "duplicate": false }. - Repetição do mesmo
event_id:200com{ "duplicate": true }; nada novo é gravado.
Ambos significam que o evento foi registrado; trate qualquer um como sucesso e remova-o da fila. Gere o event_id quando o evento acontecer, guarde-o junto com o evento enfileirado e nunca o regenere em uma nova tentativa, para que um reenvio após um timeout não conte em duplicidade.
Mensagens pausadas, expiradas e excluídas ainda aceitam eventos, de modo que uma interação com uma mensagem recém-pausada não se perde.
Comportamento offline
- Enfileire cada evento com seu
event_ideoccurred_ate envie a fila quando a conectividade voltar, do mais antigo para o mais novo. - Mantenha o
occurred_atreal. Após 7 dias o servidor o substitui pelo horário de recebimento, o que move o evento para o dia errado nas métricas e desloca a frequênciainterval; por isso descarte eventos enfileirados com essa idade em vez de enviá-los tarde. - Evite chamar
eligibleenquanto ainda houver eventos na fila: envie a fila primeiro, caso contrário o servidor pode retornar uma mensagem que o dispositivo já exibiu.
Erros e limites
Os corpos de erro usam o envelope padrão, {"error": {"code", "message", "details"}}.
| Status | Código | Rota | Significado e comportamento do SDK |
|---|---|---|---|
| 401 | unauthorized | ambas | Chave de cliente ausente ou inválida. Não tente novamente. |
| 403 | forbidden | ambas | A chave não é uma chave de cliente deste App, ou o par device_id/token não confere. O mesmo código cobre um token errado, um dispositivo desconhecido, um dispositivo excluído e um dispositivo de outro App. Em events, um App desativado no painel também responde 403 (eligible responde uma lista vazia). Registre o dispositivo novamente, não tente de novo como está. |
| 404 | not_found | events | A mensagem não existe neste App. Descarte o evento. |
| 422 | invalid_body | ambas | JSON malformado, campo desconhecido, device_id ou token ausente, ou occurred_at inválido. Corrija o cliente, não tente novamente. |
| 422 | invalid_content | events | O evento é inválido. details.fields mapeia cada campo para um código: event_id: invalid_id, type: invalid_type, button_id: required, not_allowed ou unknown_button (também um clique em image para uma mensagem sem image_action), reason: invalid_reason ou not_allowed. Descarte o evento. |
| 429 | rate_limited | ambas | Limite por dispositivo atingido. Aguarde o header Retry-After (segundos) e tente novamente com o mesmo event_id. |
| 5xx | - | ambas | Tente novamente com backoff e o mesmo event_id. |
Limites por dispositivo, por minuto: 30 em eligible e 120 em events. Esses valores iniciais podem mudar; o header Retry-After é a fonte da verdade. Agrupe seu trabalho: um eligible por sessão fica muito abaixo do limite.
O que as métricas significam
O painel e o endpoint de métricas são calculados a partir dos eventos acima, sobre uma janela de dias no fuso horário do App, pelo occurred_at gravado do evento.
| Métrica | Definição | Alimentada por |
|---|---|---|
| Impressões | Contagem de eventos impression. | impression |
| Alcance | Dispositivos distintos com pelo menos uma impressão. | impression |
| Cliques | Contagem de eventos click, também por button_id. Os toques na imagem são a linha image, listada primeiro. | click |
| Clicadores únicos | Dispositivos distintos com pelo menos um clique. | click |
| CTR | Clicadores únicos / alcance, em porcentagem com uma casa decimal. null quando o alcance é 0. | impression, click |
| Taxa de dispensa | Dispositivos que fecharam a mensagem por conta própria (close_button, swipe, outside_tap) e nunca clicaram / alcance, em porcentagem. null quando o alcance é 0. Um timeout não é rejeição e não é contado. | impression, click, dismiss |
| Fechadas automaticamente | Dispositivos cuja única saída foi o temporizador (fechamento auto, nenhum fechamento manual, nenhum clique). Informado como contagem, separado da taxa de dispensa. | impression, click, dismiss |
| Média de impressões por dispositivo | Impressões / alcance. | impression |
| Dispositivos elegíveis | O tamanho do público segmentado (segmento e plataformas) agora. Uma estimativa pontual, não uma contagem histórica; null para uma mensagem excluída. | nenhuma |
| Taxa de alcance | Alcance / dispositivos elegíveis, limitada a 100. | impression |
Notas para quem desenvolve SDKs:
- Um clique ou dismiss sem uma impressão do mesmo dispositivo na janela infla os clicadores únicos ou os dispensadores em relação ao alcance. Por isso a impressão deve ser reportada primeiro.
- Um dispositivo é contado em no máximo um entre CTR, taxa de dispensa e fechadas automaticamente, de modo que os três nunca somam mais de 100%.
- Os detalhamentos (plataforma, versão do app, país) contam dispositivos com impressão, usando os valores que o servidor leu do dispositivo quando o evento foi recebido.
- Como o alcance abrange uma janela e os dispositivos elegíveis são um instantâneo, o alcance pode exceder os dispositivos elegíveis.
MAU
Os eventos de In-App não contam como atividade para o MAU (usuários ativos mensais): registrar uma impressão, clique ou dismiss não atualiza o last_seen_at do dispositivo, portanto exibir mensagens In-App nunca altera o MAU de um App.