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

Índice

Recibos de entrega

A diferença entre "o provedor aceitou" e "chegou no aparelho" — como provamos a segunda com uma prova criptográfica, o que exatamente ela garante e o que ela não garante.

Recibos de entrega

Quase toda plataforma de push responde uma pergunta só: o provedor aceitou a mensagem? É uma informação útil, e é muito menos do que parece.

O “aceito” significa que o serviço da Google ou da Apple recebeu o pedido e assumiu a tarefa de tentar entregar. Entre esse momento e a tela do celular ainda existe: aparelho desligado, aparelho sem rede por dias, app desinstalado com o token ainda vivo na base, sistema operacional matando processos por bateria, e a própria fila do provedor descartando a mensagem quando o prazo vence. Nada disso volta para você.

A PushMesh responde a segunda pergunta: quantos aparelhos comprovadamente receberam? Não por estimativa, e não pela palavra do app: por uma prova criptográfica que só existe dentro daquele push, para aquele aparelho.


Os dois números, lado a lado

CampoO que significaDe onde vem
successfulO provedor aceitou a mensagem.Resposta do provedor no envio.
recebidosO aparelho devolveu a prova de que a notificação chegou.O próprio aparelho, com a prova conferida no servidor.

E a razão entre os dois, já calculada para você:

"pm_metricas": { "cobertura_recibo_pct": 87.4 }

Se você monta SLA, relatório de campanha ou decisão de reenvio em cima de successful, está medindo a fila do provedor. O número do aparelho é recebidos.


Como a prova funciona

No momento do envio, para cada aparelho, o servidor calcula uma assinatura e a coloca dentro do payload do push:

prova = HMAC-SHA256(chave-do-app, "<id-do-disparo>:<id-do-aparelho>")  → 32 caracteres
  • A chave usada na assinatura tem 32 bytes aleatórios, fica cifrada no registro do aplicativo e nunca sai do servidor.
  • A prova é por (disparo × aparelho): a prova de um aparelho não vale para outro, nem para outro disparo.
  • O SDK só devolve o que recebeu. Ele não calcula nada, e não teria como.
  • A conferência no servidor é feita em tempo constante, e a recusa tem sempre o mesmo texto — não existe oráculo para adivinhar uma prova por tentativa.

O ciclo inteiro:

  1. Você chama a rota de envio. O identificador do disparo entra no payload.
  2. Na saída de cada push, o servidor injeta a prova daquele aparelho.
  3. O push chega ao aparelho carregando as duas coisas.
  4. O SDK devolve as duas, exatamente como vieram.
  5. O servidor recalcula a assinatura e compara. Bateu, o recibo é contado.

É por isso que as chaves com prefixo pm_ são reservadas no seu data: se você pudesse escrever nelas, apagaria a prova da sua própria entrega.


O que isso prova — e o que não prova

Esta é a parte que decide se você pode confiar no número. Ela é chata de propósito.

O recibo prova

  • que aquele push, daquele disparo, chegou ao código do app naquele aparelho específico;
  • que a confirmação veio de quem tinha a prova — ou seja, do aparelho que recebeu a mensagem, não de um cliente inventando números.

O evento de clique prova, além disso, que a pessoa tocou na notificação. Um clique carimba a entrega junto quando ela ainda não tinha sido contada: não se clica no que não chegou.

O recibo NÃO prova

  • Que a pessoa viu. Entregue na bandeja não é lido. Ninguém no mercado mede “viu”; nós também não.
  • Que a ausência de recibo é ausência de entrega. Um aparelho desligado, sem rede ou com o app já removido não devolve nada. Falta de recibo é falta de confirmação, não prova de falha.
  • Cobertura de 100%. Ela não existe em base real, e desconfie de quem prometer. Um número abaixo de 100% é o retrato honesto da sua base.

A diferença entre Android e iOS, dita com todas as letras

No Android, a mensagem é entregue ao seu app em todos os estados (primeiro plano, segundo plano e app fechado), então o SDK confirma o recebimento na chegada.

No iOS, quem exibe a notificação é o sistema, e o seu app pode nem ser executado quando o push chega. O recibo é enviado quando o app processa a mensagem: em primeiro plano, ao tocar na notificação, ou depois — a fila offline do SDK reenvia o que ficou para trás no próximo arranque.

Consequência prática, e é importante que você entre nisso sabendo: a cobertura medida no iOS é estruturalmente menor que a do Android. Não é defeito de medição, é o que o sistema permite observar hoje. Compare cobertura de iOS com iOS ao longo do tempo, não com o Android do mesmo disparo.

Mais uma, sobre successful

O contrato tem um estado “entregue” separado de “enviado”, e os dois somam em successful. Hoje nenhum caminho do serviço grava “entregue” — na prática, successful é aceitação do provedor, e ponto. A prova de chegada vive exclusivamente em recebidos.


Como consultar

Um disparo

GET https://api.pushmesh.io/api/v1/notifications/{id}?app_id=…

curl 'https://api.pushmesh.io/api/v1/notifications/01926f3b-1111-7222-8333-444455556666?app_id=01926f3a-4b2c-7d8e-9f01-23456789abcd' \
  -H 'Authorization: Basic pm_live_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
{
  "id": "01926f3b-1111-7222-8333-444455556666",
  "successful": 48120,
  "failed": 74,
  "errored": 16,
  "remaining": 0,
  "converted": 3907,
  "recebidos": 42061,
  "queued_at": 1756290000,
  "send_after": null,
  "completed_at": 1756290480,
  "name": "catálogo primavera — lote 3",
  "headings": { "pt": "Catálogo de primavera no ar" },
  "contents": { "pt": "Peças novas com 20% até domingo." },
  "included_segments": ["Active Users"],
  "include_player_ids": null,
  "include_external_user_ids": null,
  "platform_delivery_stats": {
    "android": { "successful": 31004, "failed": 51 },
    "ios": { "successful": 17116, "failed": 23 }
  },
  "pm_metricas": {
    "via": "rest",
    "cobertura_recibo_pct": 87.4,
    "ttfd_ms": null,
    "duracao_ms": 480000
  }
}
CampoO que é
successfulO provedor aceitou. Não é prova de chegada.
failedRejeição permanente (token morto, por exemplo).
erroredTrês tentativas esgotadas.
remainingAinda em processamento. Tem de zerar no fim; enquanto não zera, o veredito não está fechado.
convertedCliques confirmados pelo aparelho. 0 quer dizer ninguém clicou.
recebidosA prova de chegada.
queued_at, send_after, completed_atEpoch em segundos.
included_segmentsSempre array — vazio, nunca null, quando o alvo foi lista.
platform_delivery_statsQuebra por plataforma da aceitação do provedor.
pm_metricas.cobertura_recibo_pctrecebidos ÷ successful, com uma casa decimal.
pm_metricas.duracao_msDo enfileiramento à conclusão.
pm_metricas.ttfd_msHoje é sempre null. Está no shape, não é medição.

Três comportamentos que valem saber antes de escrever o cliente:

  • Repetir a mesma pergunta é de graça. Consultas do mesmo disparo dentro de um segundo são servidas de cache e não contam no teto de 1.000 consultas por segundo. O que estoura o teto é pedir coisas diferentes rápido demais.
  • 404 também significa “de outro aplicativo”. A rota nunca confirma a existência de disparo alheio.
  • Se você ligou webhook ou stream, esta rota responde 403. É um canal ou outro, nunca os dois — ver a última seção.

No plano gratuito

recebidos vem como null, acompanhado de uma explicação estruturada:

"recebidos": null,
"recebidos_gate": {
  "plano_atual": "free",
  "habilita_em": "pro",
  "por_que": "confirmação de entrega real é recurso do plano PRO"
}

Nunca é um erro HTTP. Mas cuidado com a leitura defensiva: um ?? 0 transforma “não disponível no seu plano” em “zero entregas”.

Uma janela de tempo

GET https://api.pushmesh.io/api/v1/notifications/stats?app_id=…&desde=…&ate=…

Para fechar campanha, relatório diário ou ciclo de faturamento sem uma chamada por disparo.

curl 'https://api.pushmesh.io/api/v1/notifications/stats?app_id=01926f3a-4b2c-7d8e-9f01-23456789abcd&desde=2026-08-01T00:00:00-03:00&ate=2026-09-01T00:00:00-03:00' \
  -H 'Authorization: Basic pm_live_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
{
  "desde": "2026-08-01T03:00:00+00:00",
  "ate": "2026-09-01T03:00:00+00:00",
  "totais": {
    "disparos": 214,
    "enviados": 4120334,
    "falhas": 8871,
    "entregues": 3604221,
    "cliques": 288940
  },
  "por_dia": [
    { "dia": "2026-08-01", "enviados": 130221, "entregues": 114003, "cliques": 9140 }
  ]
}
  • desde e ate são obrigatórios, em RFC 3339 com offset, e a janela é semiaberta: inclui o início, exclui o fim. ate precisa ser depois de desde.
  • enviados é aceitação do provedor; entregues é recibo real; cliques são cliques confirmados.
  • Os números vêm dos contadores de cada disparo, que sobrevivem à rotatividade do histórico detalhado — a janela funciona para qualquer período, antigo ou recente.
  • O dia de por_dia é calculado num fuso fixo de UTC−03, sem horário de verão. Se o seu fechamento é em outro fuso, faça o recorte com desde/ate e use os totais.
  • por_dia é best-effort: numa falha da consulta da curva ele volta vazio, enquanto os totais continuam corretos. Não interprete lista vazia como “nenhum dia teve envio” sem olhar os totais.

Esta rota continua respondendo mesmo com webhook ou stream ligados — é a única leitura que sobrevive à troca de canal.


A rota que o aparelho chama

POST https://api.pushmesh.io/api/v1/receipts

Você não precisa chamar esta rota: o SDK já faz isso sozinho, incluindo fila offline, deduplicação local e o evento de clique. Ela está documentada porque quem escreve um SDK próprio precisa dela — e porque um contrato de entrega que você não pode inspecionar não é um contrato.

{
  "app_id": "01926f3a-4b2c-7d8e-9f01-23456789abcd",
  "notification_id": "01926f3b-1111-7222-8333-444455556666",
  "player_id": "01926f4c-7a1b-7c2d-8e3f-a1b2c3d4e5f6",
  "rcpt": "9f4c2ab1e0d7358a6b1c4f902e77a13d",
  "evento": "recebido"
}
CampoTipoObrigatórioO que é
app_idUUIDsimO aplicativo.
notification_idUUIDsimO identificador do disparo, como veio no push.
player_idUUIDsimO id público do aparelho.
rcptstringsimA prova, ecoada como veio. Até 64 caracteres.
eventostringnão"recebido" (padrão) ou "clique".

Sem credencial: a prova é a credencial. O que protege a rota é a própria assinatura, mais um teto de 120 requisições por minuto por IP (este é o único 429 da API que traz Retry-After: 60) e um corpo máximo de 16 KB.

As respostas

RespostaSignificadoO que o cliente faz
{"ok": true}Contado agora.Descarta o item da fila.
{"ok": true, "duplicado": true}Já estava contado (reenvio, ou o provedor entregou duas vezes).Descarta o item.
{"ok": true, "tardio": true}O histórico detalhado daquele dia já foi arquivado, mas o recibo foi registrado e o contador subiu.Descarta o item.

duplicado e tardio são coisas diferentes, e tratar um como o outro é como se perde recibo em silêncio: duplicado significa “já tínhamos”, tardio significa “não tínhamos onde guardar, e guardamos em outro lugar — contou”.

A idempotência é garantida em três camadas independentes, e a última delas é o banco: o mesmo recibo enviado dez vezes conta uma.

A janela de aceitação é de 90 dias. Ela cobre com folga o atraso legítimo máximo — um agendamento de até 28 dias somado a um prazo de entrega de até 28 dias — porque o clique pode vir de uma notificação parada na bandeja por semanas.

Erros

CódigoMotivo
400JSON inválido, campo faltando, UUID malformado, rcpt vazio ou acima de 64 caracteres, evento diferente de recebido/clique, app_id inexistente.
403Prova inválida (texto sempre idêntico, de propósito), ou aplicativo pausado.
413Corpo acima de 16 KB.
429Mais de 120 requisições por minuto do mesmo IP, com Retry-After: 60.
503Banco indisponível.

Nunca calcule a prova no cliente. A chave nunca sai do servidor, e a recusa não distingue “prova errada” de “disparo inexistente” — é assim de propósito.


O que fazer com o dado

O recibo só vale o que você faz com ele. Quatro usos que aparecem rápido:

  1. Separar “não mandei” de “não chegou”. Com successful alto e recebidos baixo em um segmento específico, o problema não é a sua campanha: é uma versão do app, uma marca de aparelho ou uma configuração de bateria.
  2. Medir o clique contra a realidade. Taxa de clique sobre recebidos é uma medida de mensagem; sobre enviados, é uma medida de mensagem misturada com a saúde da base. A primeira é acionável, a segunda oscila sozinha.
  3. Parar de pagar por fantasma. Aparelho que nunca devolve recibo por semanas seguidas, mas continua “aceito” pelo provedor, é candidato natural a limpeza — cruze com a exportação da base.
  4. Acompanhar a cobertura como um sinal de saúde. Uma queda súbita de cobertura, com o mesmo público, quase sempre é uma versão nova do seu app quebrando o recebimento — e é o tipo de problema que, sem recibo, você só descobre pela queda de receita.

Ser avisado, em vez de perguntar

Se você prefere não consultar, cada recibo pode virar um evento no seu servidor: delivery.received e delivery.clicked são criados na mesma transação do fato — não existe “aconteceu mas não avisou”.

A entrega é at-least-once: deduplique pelo identificador da tentativa que vem no cabeçalho de cada entrega. A assinatura é HMAC-SHA256 sobre o corpo bruto, então confira antes de reserializar o JSON. Os detalhes estão na página de webhooks.

Uma consequência que pega gente de surpresa: ligar o webhook desliga a consulta. As rotas de leitura de disparo passam a responder 403 na hora — é um canal ou outro, nunca os dois, para o mesmo dado não ser cobrado duas vezes. A rota de janela de tempo continua funcionando.