Mensagens In-App
Campanhas in-app pela API — formatos, estados, gatilhos, blocos, o funil de eventos e a negociação de capacidade por versão do SDK.
Mensagens In-App
Mensagem in-app é o cartão que aparece dentro do seu aplicativo, com a pessoa já ali — sem depender de permissão de notificação. É o canal que funciona justamente com quem bloqueou o push.
Tudo o que o painel faz, esta API faz: as rotas de gestão chamam o mesmo núcleo que a interface. Não existe recurso “só do painel” — você pode criar, publicar, medir e encerrar campanhas inteiramente pelo seu código, com a mesma chave que usa para push.
O modelo em uma frase
O servidor segmenta e assina; o aparelho decide exibir.
Quando o app abre, ele busca um pacote de sessão com as mensagens elegíveis — cada uma já com as regras completas dentro (gatilho, limites, prioridade, janela) e uma prova assinada. O pacote vale por até 24 horas no aparelho, e é o aparelho que avalia o gatilho, sem ida e volta na hora de mostrar.
Isso explica três comportamentos que surpreendem quem espera um modelo servidor-manda:
| Consequência | O que significa na prática |
|---|---|
| Pausar vale para pacotes novos | quem já baixou o pacote pode ver a mensagem por até 24 h |
| O funil só sobe com recibo provado | os números podem subcontar, nunca inflar |
| “Servido” nunca é “exibido” | servido = entrou no pacote; exibido = foi desenhado na tela |
Começo rápido
1. Ligue o canal (ele nasce desligado)
Esta é a causa número um de “criei a campanha e não aparece nada”. O canal In-App vem desligado em todo aplicativo novo — de propósito, para você preparar tudo sem ninguém receber.
curl -X PUT https://api.pushmesh.io/api/v1/in_app_messages/config \
-H "Authorization: Basic $PUSHMESH_KEY" \
-H "Content-Type: application/json" \
-d '{ "app_id": "'"$APP_ID"'", "inapp_ativo": true }'
{ "inapp_ativo": true, "inapp_cap_horas": 6 }
2. Crie a campanha (nasce em rascunho)
curl -X POST https://api.pushmesh.io/api/v1/in_app_messages \
-H "Authorization: Basic $PUSHMESH_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_id": "'"$APP_ID"'",
"nome": "boas-vindas",
"formato": "modal",
"alvo_tipo": "todos",
"alvo": [],
"conteudo": {
"pt": {
"titulo": "Que bom te ver por aqui",
"corpo": "Ative os avisos e saiba na hora quando seu pedido sair.",
"botao": { "texto": "Ativar avisos", "acao": "open_notification_settings" }
}
},
"gatilho": { "tipo": "ao_abrir", "atraso_ms": 2000 },
"regras": { "cap_total": 1, "cooldown_h": 24 }
}'
Resposta 201, com estado: "rascunho". Criar nunca publica.
3. Confira o alcance antes de publicar
curl "https://api.pushmesh.io/api/v1/in_app_messages/$CID/alcance?app_id=$APP_ID" \
-H "Authorization: Basic $PUSHMESH_KEY"
{
"campanha_id": "0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40",
"alvo_tipo": "todos",
"alcance_estimado": 48120,
"schema_min_efetivo": 1,
"alcancaveis_agora": 47010,
"precisam_app_atualizado": 1110,
"sem_versao_reportada": 402
}
4. Publique
curl -X POST "https://api.pushmesh.io/api/v1/in_app_messages/$CID/publicar?app_id=$APP_ID" \
-H "Authorization: Basic $PUSHMESH_KEY"
Do lado do aplicativo não há nada a fazer: o SDK React Native já busca o pacote e devolve os recibos sozinho.
Estados e transições
São quatro estados. A campanha nasce em rascunho e só sai dele por uma
chamada explícita.
| Estado | Serve mensagem? | Editável? |
|---|---|---|
rascunho | não | sim |
ativa | sim | sim |
pausada | não (só pacotes novos) | sim |
encerrada | não | não — é terminal |
| Ação | De | Para |
|---|---|---|
POST /{id}/publicar | rascunho, pausada | ativa |
POST /{id}/pausar | ativa | pausada |
POST /{id}/encerrar | rascunho, ativa, pausada | encerrada |
Transição inválida responde 409 dizendo o estado atual e as origens aceitas — por exemplo: campanha está ‘encerrada’ — esta ação só vale a partir de: rascunho, pausada.
Encerrar preserva o funil. Excluir apaga tudo. Campanha encerrada não pode
mais ser editada (o PATCH responde 409); a saída oficial para reaproveitar o
criativo é POST /{id}/duplicar, que devolve uma cópia em rascunho com os
contadores zerados.
Excluir uma campanha ativa exige confirmação explícita:
curl -X DELETE "https://api.pushmesh.io/api/v1/in_app_messages/$CID?app_id=$APP_ID&confirmar=true" \
-H "Authorization: Basic $PUSHMESH_KEY"
Sem confirmar=true, a resposta é 409 — porque excluir apaga o funil e o
rastro para sempre.
Formatos
| Formato | Como aparece |
|---|---|
modal | cartão centralizado, com véu escurecendo o fundo |
banner_top | faixa no topo |
banner_bottom | faixa no rodapé |
fullscreen | ocupa a tela inteira |
Conteúdo
conteudo é um objeto por idioma, e o bloco pt é obrigatório. Mandar
titulo na raiz de conteudo é erro — ele precisa estar dentro de um idioma.
Cada bloco de idioma usa uma das duas formas. Misturar as duas é 422.
Forma A — campos fixos
| Campo | Tipo | Limite | O que faz |
|---|---|---|---|
titulo | texto | 80 chars | título do cartão |
corpo | texto | 240 chars | texto principal |
imagem | URL | https:// | arte do cartão |
botao | objeto | — | { texto (1–24), acao } |
botao_secundario | objeto | — | só existe ao lado de botao |
acao_card | ação | — | toque na área do cartão |
Regra de validade: pelo menos um entre imagem, titulo, corpo e
botao. Cartão vazio não existe.
Forma B — pilha de blocos
blocos é um array ordenado. Limites por campanha:
| Bloco | Máximo | Campos |
|---|---|---|
texto | 8 | texto (1–500), cor (#rrggbb), tipografia |
imagem | 4 | url (https, obrigatória), sangra, raio (0–64), acao_click, margens (0–64 cada) |
botao | 2 | texto (1–24), acao (obrigatória), cor_fundo, cor_texto, raio (0–32), tipografia |
espacador | 8 | altura (1–200) |
Teto de 12 blocos no total, com pelo menos um bloco visível.
Tipografia (em texto e botao):
| Campo | Valores |
|---|---|
fonte | sistema, serifada, monoespacada |
peso | regular, medio, negrito |
italico, sublinhado | booleanos |
tamanho | 10 a 48 |
alinhamento | esquerda, centro, direita |
Ações de botão
A lista é fechada — qualquer outro valor é recusado na criação:
| Ação | O que faz |
|---|---|
dismiss | fecha a mensagem |
open_url | abre um endereço |
open_notification_settings | abre a tela de notificações do sistema |
open_url exige um esquema (https://... ou meuapp://...) e recusa
http:// puro e endereços com espaço.
open_notification_settings é o motor da reativação: é o botão que leva quem
bloqueou o push de volta aos ajustes. Quando o aparelho volta reportando a
permissão ligada, o servidor atribui essa volta às campanhas de reativação dos
últimos 7 dias — é o campo reativados das estatísticas.
Estilo e a regra anti-trava
estilo aceita cores em #rrggbb (fundo, texto, botao_fundo,
botao_texto), raio (0–32), escurecer (0–1), padding_px (0–64),
imagem_sangra, fundo_imagem (https) e acao_card.
O bloco close controla o X de fechar: mostrar, icone (x, x_circulo,
seta), tamanho (16–48), cor e fundo.
Esconder o X só passa se houver outra saída garantida — formato modal (o
véu dispensa) ou um botão / ação de card com dismiss. Caso contrário a
criação é recusada, dizendo que a mensagem prenderia o usuário. Não é
preferência de design: é a diferença entre uma campanha e um aplicativo
travado.
Gatilhos — quando a mensagem aparece
O gatilho vai na forma plana. Campo que não pertence ao tipo é recusado, e
mandar qualquer parâmetro sem tipo também é — só atraso_ms viaja sozinho.
| Tipo | Parâmetros | Quando dispara |
|---|---|---|
ao_abrir | — | na abertura do app |
evento | chave, comparador, valor | quando o app sinaliza aquele estado |
sessao_duracao | segundos (1–86400) | após N segundos na sessão |
inatividade | horas (1–8760) | após N horas sem abrir |
atraso_ms (0 a 60000, padrão 2000) conta a partir do instante em que a
condição casou.
Comparadores do gatilho evento
| Comparador | Aceita valor | Tipo do valor |
|---|---|---|
igual | sim | texto ou número |
diferente | sim | texto ou número |
maior | sim | só número |
menor | sim | só número |
existe | não | — |
nao_existe | não | — |
chave: 1 a 64 caracteres, apenas letras, números e_ . : -. Espaço não é aceito — espaço em nome de chave é erro de digitação silencioso, e erro silencioso é mensagem que nunca aparece.valortexto: 1 a 120 caracteres.valornúmero: módulo até 1e12, no máximo 6 casas decimais.- O tipo importa:
"10"(texto) e10(número) não são a mesma coisa na comparação. maioremenorcom texto são recusados na criação — nunca chegam ao aparelho.
No aplicativo, o gatilho de evento é alimentado pelo SDK:
PushMesh.definirGatilho('tela', 'checkout');
Regras de frequência — três freios
| Regra | Onde vive | Padrão | O que faz |
|---|---|---|---|
cap_total | campanha | 1 | quantas vezes esta campanha aparece (0 = sem limite) |
cooldown_h | campanha | 24 | horas mínimas entre duas exibições desta campanha |
inapp_cap_horas | aplicativo | 6 | horas mínimas entre quaisquer duas mensagens in-app |
O terceiro é o que costuma passar despercebido: mesmo com cap_total: 0
(“ilimitado”), o aparelho não mostra dois in-apps em menos de 6 horas, de
campanha nenhuma. Só inapp_cap_horas: 0 desliga esse freio (faixa aceita: 0 a
168).
Ainda em regras: fechar_apos_ms (0 = não fecha sozinho; ou 1 a 600000) e
prioridade (padrão 100, maior vence).
A janela de exibição usa inicia_em e termina_em no topo do corpo, em
RFC3339 — não dentro de regras. Mandá-los dentro de regras é recusado, para
você não achar que agendou quando não agendou.
Público
alvo_tipo | Quem entra | alvo |
|---|---|---|
todos | toda a base | [] |
bloqueados | quem negou a permissão de push | [] |
nunca_pediu | quem nunca viu o pedido de permissão | [] |
desinscritos | quem se descadastrou | [] |
external_ids | lista de identificadores seus | 1+ itens |
player_ids | lista de aparelhos | 1+ itens |
Os quatro segmentos exigem alvo: []; as duas listas exigem pelo menos um
item. Lista não-vazia num segmento é erro.
Capacidade por versão do SDK — a parte que ninguém gosta de contar
Recurso novo exige um aplicativo capaz de obedecê-lo. Um aparelho rodando uma versão antiga do SDK não recebe a campanha — ela é suprimida e contada, em vez de servida pela metade.
| Recurso | Exige |
|---|---|
Campos fixos, ao_abrir | SDK 0.5+ |
| Blocos | SDK 0.5+ |
evento, sessao_duracao, inatividade | SDK 0.6+ |
inicia_em (agendamento) enquanto a hora não chega | SDK 0.6+ |
O SDK atual é o @pushmesh/sdk@0.7.2 — quem instala hoje já cobre tudo. O
problema não é o SDK: é a fatia da sua base que ainda roda uma versão
publicada meses atrás.
Por isso /alcance responde separado, e não com um número único:
| Campo | Leitura honesta |
|---|---|
alcance_estimado | todo mundo que casa com o público |
alcancaveis_agora | quem realmente vai receber |
precisam_app_atualizado | quem só receberá quando atualizar o app |
sem_versao_reportada | aparelhos que sequer informam a versão |
alcance_estimado é sempre a soma exata de alcancaveis_agora e
precisam_app_atualizado, e sem_versao_reportada é um subconjunto do
segundo — ele sai destacado para não acusar de desatualizado quem talvez não
seja.
Aparelho que não reporta a versão conta como incapaz, não como capaz: sem prova, não entra na promessa de alcance.
schema_min_efetivo é recalculado a cada pergunta, porque a parte do
agendamento é móvel: uma campanha marcada para as 21h exige SDK 0.6+ hoje e
volta a alcançar todo mundo depois das 21h.
Antes de culpar a entrega, olhe precisam_app_atualizado.
O funil
curl "https://api.pushmesh.io/api/v1/in_app_messages/$CID/funil?app_id=$APP_ID" \
-H "Authorization: Basic $PUSHMESH_KEY"
{
"campanha_id": "0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40",
"nome": "boas-vindas",
"estado": "ativa",
"servidos": 12840,
"impressoes": 9012,
"cliques": 1877,
"fechados": 6410,
"alcance_estimado": 48120
}
| Número | O que é |
|---|---|
servidos | entrou no pacote de sessão de um aparelho |
impressoes | foi desenhado na tela (recibo provado) |
cliques | alguém tocou (recibo provado) |
fechados | alguém fechou (recibo provado) |
servidos maior que impressoes é o esperado, não um defeito: o pacote foi
entregue e o gatilho não casou naquela sessão. Só servidos sobe no servidor;
os outros três dependem de prova vinda do aparelho — por isso o funil pode
subcontar, nunca inflar.
Números por janela de tempo
Para casar com o seu ciclo de relatório:
curl "https://api.pushmesh.io/api/v1/in_app_messages/stats?app_id=$APP_ID&desde=2026-08-01T00:00:00Z&ate=2026-09-01T00:00:00Z" \
-H "Authorization: Basic $PUSHMESH_KEY"
{
"desde": "2026-08-01T00:00:00+00:00",
"ate": "2026-09-01T00:00:00+00:00",
"totais": { "impressoes": 40122, "cliques": 8110, "reativados": 512 },
"campanhas": [
{
"campanha_id": "0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40",
"nome": "boas-vindas",
"estado": "ativa",
"alvo_tipo": "todos",
"impressoes": 9012,
"cliques": 1877,
"reativados": 208
}
]
}
desde e ate são obrigatórios, em RFC3339, e a janela é semiaberta
[desde, ate). A ordenação é por impressões na janela.
reativados não é um evento do aparelho: é atribuição do servidor. Quando o
aparelho volta a reportar a permissão de notificação ligada, as ocasiões
dele dos últimos 7 dias em campanhas de reativação são carimbadas. É a resposta
para “essa campanha trouxe alguém de volta?”.
As duas rotas do aparelho
Você normalmente não chama estas — o SDK cuida delas. Estão documentadas porque quem escreve um cliente próprio precisa do contrato.
GET /api/v1/inapp/pending
Entrega o pacote da sessão. Sem chave de API (roda dentro do app do usuário
final), com app_id, player_id e schema na query, e teto de 120
requisições por minuto por IP.
Responde 200 com pacote_id, messages[], validade_s (86400),
cap_global_h e etag; aceita If-None-Match e responde 304 quando nada
mudou. Lista vazia é normal e nunca é 404 — inclusive para um player_id
desconhecido, porque a rota não confirma existência de aparelho.
O pacote traz no máximo 20 campanhas, ordenadas por prioridade. Campanha de prioridade baixa numa base com muitas campanhas ativas pode simplesmente não viajar.
POST /api/v1/inapp/eventos
Recibos em lote — a única fonte de impressões, cliques e fechamentos.
Cada item leva campanha_id, prova (ecoada como veio no pacote), evento
(impressao, clique ou fechou), ocorrencia_n (1 a 500) e em (RFC3339,
dentro da janela de 7 dias atrás até 5 minutos à frente). Lote de 1 a 50 itens.
A resposta é posicional, na mesma ordem dos itens:
{ "ok": true, "resultados": ["aceito", "duplicado", "prova_invalida", "acima_do_cap"] }
| Veredito | Significa |
|---|---|
aceito | gravado, contadores subiram |
duplicado | esse evento dessa ocorrência já existia |
prova_invalida | a prova não confere com nenhum pacote recente do aparelho |
acima_do_cap | ocorrencia_n passou do limite da campanha |
Atenção ao contraste: erro de forma em um item derruba a requisição inteira (400/422). Só a prova tem veredito por item. Valide a forma na sua fila local antes de enviar, senão um carimbo de tempo fora da janela leva junto os 49 itens sadios.
Erros
| Status | Quando |
|---|---|
| 400 | forma inválida: JSON quebrado, identificador malformado, datas fora do RFC3339, lote fora de 1–50, evento desconhecido |
| 401 | chave ausente, desconhecida, revogada ou expirada — ou app_id que não é o da chave |
| 403 | aplicativo pausado, canal In-App desligado, ou lote inteiro com prova inválida |
| 404 | campanha inexistente neste aplicativo |
| 409 | transição de estado inválida, editar campanha encerrada, excluir campanha ativa sem confirmar |
| 413 | corpo acima de 16 KB (rotas de aparelho) |
| 422 | campanha inválida, inapp_cap_horas fora de 0–168, carimbo de tempo fora da janela |
| 429 | mais de 120 requisições por minuto por IP (rotas de aparelho), com Retry-After: 60 |
| 503 | dependência indisponível — sempre nomeada |
Todo erro sai no mesmo envelope, com causa e como_corrigir. Nos erros de
validação de campanha, causa lista cada campo com o problema e
como_corrigir diz o que fazer em cada um.
app_id que não é o da chave responde 401, nunca 404 — a API não confirma
a existência de recursos de outro aplicativo.
Armadilhas que valem cinco minutos
- O canal nasce desligado. Campanha ativa + canal desligado = nada
acontece. Ligue com
PUT /in_app_messages/config. - Criar não publica. A campanha nasce em rascunho.
- Pausar não recolhe o que já foi entregue. O pacote vive até 24 h no aparelho.
app_idé obrigatório em todas as rotas de gestão, na query ou no corpo.- Campo de gatilho que não pertence ao tipo é erro, nunca ignorado.
inicia_em/termina_emficam no topo, não dentro deregras.- Não misture blocos com campos fixos no mesmo bloco de idioma.
- Gatilho novo corta alcance. Confira
precisam_app_atualizado. servidos>impressoesé normal.acima_do_capvem dentro de um 200. Quem só olha o status HTTP acha que contou.- O cap global do aplicativo (6 h por padrão) manda mesmo em campanha “ilimitada”.