PushMesh
Entrar Solicitar acesso
Abrir navegação das seções

Índice

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ênciaO que significa na prática
Pausar vale para pacotes novosquem já baixou o pacote pode ver a mensagem por até 24 h
O funil só sobe com recibo provadoos 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.

EstadoServe mensagem?Editável?
rascunhonãosim
ativasimsim
pausadanão (só pacotes novos)sim
encerradanãonão — é terminal
AçãoDePara
POST /{id}/publicarrascunho, pausadaativa
POST /{id}/pausarativapausada
POST /{id}/encerrarrascunho, ativa, pausadaencerrada

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

FormatoComo aparece
modalcartão centralizado, com véu escurecendo o fundo
banner_topfaixa no topo
banner_bottomfaixa no rodapé
fullscreenocupa 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

CampoTipoLimiteO que faz
titulotexto80 charstítulo do cartão
corpotexto240 charstexto principal
imagemURLhttps://arte do cartão
botaoobjeto{ texto (1–24), acao }
botao_secundarioobjetosó existe ao lado de botao
acao_cardaçãotoque 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:

BlocoMáximoCampos
texto8texto (1–500), cor (#rrggbb), tipografia
imagem4url (https, obrigatória), sangra, raio (0–64), acao_click, margens (0–64 cada)
botao2texto (1–24), acao (obrigatória), cor_fundo, cor_texto, raio (0–32), tipografia
espacador8altura (1–200)

Teto de 12 blocos no total, com pelo menos um bloco visível.

Tipografia (em texto e botao):

CampoValores
fontesistema, serifada, monoespacada
pesoregular, medio, negrito
italico, sublinhadobooleanos
tamanho10 a 48
alinhamentoesquerda, centro, direita

Ações de botão

A lista é fechada — qualquer outro valor é recusado na criação:

AçãoO que faz
dismissfecha a mensagem
open_urlabre um endereço
open_notification_settingsabre 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.

TipoParâmetrosQuando dispara
ao_abrirna abertura do app
eventochave, comparador, valorquando o app sinaliza aquele estado
sessao_duracaosegundos (1–86400)após N segundos na sessão
inatividadehoras (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

ComparadorAceita valorTipo do valor
igualsimtexto ou número
diferentesimtexto ou número
maiorsimsó número
menorsimsó número
existenão
nao_existenã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.
  • valor texto: 1 a 120 caracteres. valor número: módulo até 1e12, no máximo 6 casas decimais.
  • O tipo importa: "10" (texto) e 10 (número) não são a mesma coisa na comparação.
  • maior e menor com 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

RegraOnde vivePadrãoO que faz
cap_totalcampanha1quantas vezes esta campanha aparece (0 = sem limite)
cooldown_hcampanha24horas mínimas entre duas exibições desta campanha
inapp_cap_horasaplicativo6horas 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_tipoQuem entraalvo
todostoda a base[]
bloqueadosquem negou a permissão de push[]
nunca_pediuquem nunca viu o pedido de permissão[]
desinscritosquem se descadastrou[]
external_idslista de identificadores seus1+ itens
player_idslista de aparelhos1+ 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.

RecursoExige
Campos fixos, ao_abrirSDK 0.5+
BlocosSDK 0.5+
evento, sessao_duracao, inatividadeSDK 0.6+
inicia_em (agendamento) enquanto a hora não chegaSDK 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:

CampoLeitura honesta
alcance_estimadotodo mundo que casa com o público
alcancaveis_agoraquem realmente vai receber
precisam_app_atualizadoquem só receberá quando atualizar o app
sem_versao_reportadaaparelhos 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úmeroO que é
servidosentrou no pacote de sessão de um aparelho
impressoesfoi desenhado na tela (recibo provado)
cliquesalguém tocou (recibo provado)
fechadosalgué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"] }
VereditoSignifica
aceitogravado, contadores subiram
duplicadoesse evento dessa ocorrência já existia
prova_invalidaa prova não confere com nenhum pacote recente do aparelho
acima_do_capocorrencia_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

StatusQuando
400forma inválida: JSON quebrado, identificador malformado, datas fora do RFC3339, lote fora de 1–50, evento desconhecido
401chave ausente, desconhecida, revogada ou expirada — ou app_id que não é o da chave
403aplicativo pausado, canal In-App desligado, ou lote inteiro com prova inválida
404campanha inexistente neste aplicativo
409transição de estado inválida, editar campanha encerrada, excluir campanha ativa sem confirmar
413corpo acima de 16 KB (rotas de aparelho)
422campanha inválida, inapp_cap_horas fora de 0–168, carimbo de tempo fora da janela
429mais de 120 requisições por minuto por IP (rotas de aparelho), com Retry-After: 60
503dependê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_em ficam no topo, não dentro de regras.
  • 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_cap vem 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”.