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

Índice

Webhooks

Receba confirmações de entrega e clique no seu servidor — eventos, formato do corpo, validação da assinatura HMAC em Node e Python, e a política de reentrega.

Webhooks

Depois que um push sai, existem duas formas de saber o que aconteceu: perguntar ou ser avisado. O webhook é a segunda — o PushMesh chama a sua URL quando o aparelho confirma que a mensagem chegou, quando a pessoa toca nela, e quando o disparo fecha.

Se você tem qualquer volume, é o caminho certo: em vez de milhares de consultas por minuto perguntando “e agora?”, você recebe o fato no instante em que ele acontece.


Antes de ligar: é um ou o outro

Consulta e webhook são mutuamente exclusivos, de propósito.

Enquanto o modo for webhook, as rotas GET /api/v1/notifications e GET /api/v1/notifications/{id} respondem 403 explicando o motivo. Se você tem uma rotina de consulta em laço rodando hoje, ela para de funcionar no instante da virada — planeje a troca.

O que continua respondendo é GET /api/v1/notifications/stats, a leitura por janela de tempo. E o painel continua mostrando tudo: a exclusividade é do canal de integração, não da visão humana.

ModoComo você recebePadrão
consultavocê chama GET /api/v1/notifications/{id}sim
webhooko PushMesh chama a sua URL
grpcvocê mantém um stream aberto

Ligando em uma chamada

curl -X PUT https://api.pushmesh.io/api/v1/confirmacoes \
  -H "Authorization: Basic $PUSHMESH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "modo": "webhook",
    "url": "https://seu-servidor.exemplo/pushmesh"
  }'
{
  "ok": true,
  "modo": "webhook",
  "segredo": "pm_whsec_4f3a…64 caracteres hexadecimais…"
}

Guarde o segredo agora. Ele aparece uma única vez e não existe rota que o leia de volta. Sem ele você não consegue validar assinatura nenhuma.

Regras conferidas na chamada:

  • a URL precisa ser https://. Confirmação de entrega em texto claro seria vazamento por desenho — outro esquema responde 400;
  • cadastrar ou trocar a URL gera um segredo novo e desativa o destino anterior. Não existe “mesma URL, mesmo segredo”: se você reenviar a URL, o segredo antigo morre;
  • modo: "webhook" exige um destino cadastrado. Sem ele, 400 — virar a chave sem destino deixaria o aplicativo sem confirmação nenhuma. Por isso os dois campos na mesma chamada funcionam: a URL é processada antes do modo.

Mandar só a url não liga o modo webhook. São duas coisas: a url cadastra o destino, o modo liga o canal.

Para ver o estado atual:

curl https://api.pushmesh.io/api/v1/confirmacoes \
  -H "Authorization: Basic $PUSHMESH_KEY"
{ "modo": "webhook", "fila_pendente": 0, "streams_ativos": 0 }

fila_pendente é o número de eventos que ainda serão tentados. Se ele cresce, seu endpoint está recusando ou demorando.


Os eventos

EventoQuando nasce
delivery.receivedo aparelho confirmou que a notificação chegou
delivery.clickeda pessoa tocou na notificação
notification.completedo disparo fechou: não há mais entrega pendente

Cadastrando pela API, o destino assina os três.

Esses eventos nascem na mesma transação do fato: ou o recibo é gravado e o aviso é enfileirado, ou nenhum dos dois. Não existe “aconteceu mas não avisou”.

Corpo de delivery.received e delivery.clicked

{
  "evento": "delivery.received",
  "app_id": "0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40",
  "notification_id": "0198f0a1-3c22-7a11-8ef0-1122334455aa",
  "player_id": "0198e9d3-15ab-7c40-91ce-77aa00bb11cc",
  "quando": "2026-08-27T15:03:41.219+00:00",
  "canal": "webhook"
}

Corpo de notification.completed

{
  "evento": "notification.completed",
  "app_id": "0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40",
  "notification_id": "0198f0a1-3c22-7a11-8ef0-1122334455aa",
  "successful": 12840,
  "failed": 12,
  "errored": 3,
  "recipients": 12855,
  "quando": "2026-08-27T15:09:02.004+00:00",
  "canal": "webhook"
}

Lembrete que vale dinheiro: successful é “o provedor aceitou”, não “o aparelho recebeu”. A prova de chegada é a contagem de delivery.received — é exatamente essa diferença que o recibo existe para medir.


Cabeçalhos de cada entrega

CabeçalhoConteúdo
Content-Typeapplication/json
X-PM-Eventoo nome do evento
X-PM-Entrega-Ididentificador único desta tentativa — sua chave de idempotência
X-PM-Canalwebhook
X-PM-Assinaturasha256=<hex do HMAC-SHA256 do corpo>

Validando a assinatura

Sem validação, qualquer um que descubra a sua URL injeta confirmação falsa na sua base. São dez linhas de código — não pule.

A assinatura é HMAC-SHA256 do corpo bruto, com o seu segredo, em hexadecimal, prefixado por sha256=.

A regra que mais quebra integração: valide sobre os bytes crus do corpo, exatamente como chegaram. Se o seu framework desserializa o JSON e você re-serializa para conferir, os bytes mudam e a assinatura nunca bate. O campo canal é inserido no corpo antes da assinatura, então ele faz parte do que foi assinado.

Compare em tempo constante. Comparação com == vaza o segredo byte a byte para quem tiver paciência.

Node.js (Express)

const express = require('express');
const crypto = require('crypto');

const app = express();
const SEGREDO = process.env.PUSHMESH_WEBHOOK_SECRET;

// express.raw — NÃO use express.json() nesta rota:
// o corpo precisa chegar como veio, byte a byte.
app.post('/pushmesh', express.raw({ type: 'application/json' }), (req, res) => {
  const recebida = req.get('X-PM-Assinatura') || '';
  const esperada =
    'sha256=' + crypto.createHmac('sha256', SEGREDO).update(req.body).digest('hex');

  const a = Buffer.from(recebida);
  const b = Buffer.from(esperada);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('assinatura inválida');
  }

  const entregaId = req.get('X-PM-Entrega-Id');
  if (jaProcessado(entregaId)) return res.sendStatus(200); // reentrega: ignore

  const evento = JSON.parse(req.body.toString('utf8'));

  switch (evento.evento) {
    case 'delivery.received':
      marcarEntregue(evento.notification_id, evento.player_id, evento.quando);
      break;
    case 'delivery.clicked':
      marcarClique(evento.notification_id, evento.player_id, evento.quando);
      break;
    case 'notification.completed':
      fecharCampanha(evento.notification_id, evento);
      break;
  }

  registrarProcessado(entregaId);
  res.sendStatus(200); // 2xx = pode tirar da fila
});

app.listen(3000);

Python (Flask)

import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)
SEGREDO = os.environ["PUSHMESH_WEBHOOK_SECRET"].encode()


@app.post("/pushmesh")
def pushmesh():
    # get_data() devolve os BYTES CRUS — é sobre eles que a assinatura vale.
    corpo = request.get_data()

    esperada = "sha256=" + hmac.new(SEGREDO, corpo, hashlib.sha256).hexdigest()
    recebida = request.headers.get("X-PM-Assinatura", "")

    if not hmac.compare_digest(esperada, recebida):
        abort(401)

    entrega_id = request.headers.get("X-PM-Entrega-Id")
    if ja_processado(entrega_id):
        return "", 200  # reentrega: ignore

    evento = request.get_json()

    if evento["evento"] == "delivery.received":
        marcar_entregue(evento["notification_id"], evento["player_id"], evento["quando"])
    elif evento["evento"] == "delivery.clicked":
        marcar_clique(evento["notification_id"], evento["player_id"], evento["quando"])
    elif evento["evento"] == "notification.completed":
        fechar_campanha(evento["notification_id"], evento)

    registrar_processado(entrega_id)
    return "", 200  # 2xx = pode tirar da fila

Conferindo à mão

printf '%s' "$CORPO_BRUTO" | openssl dgst -sha256 -hmac "$PUSHMESH_WEBHOOK_SECRET"

O hexadecimal impresso é o que vem depois de sha256= no cabeçalho.


Política de reentrega

A entrega é at-least-once: o evento só sai da fila depois do 2xx do seu servidor. Timeout, 5xx, erro de DNS — tudo reagenda.

ComportamentoValor
Tempo limite de cada tentativa10 segundos
Intervalo entre tentativas1 s → 10 s → 60 s → 5 min → 15 min (teto)
Tentativas até desistir30 (aproximadamente 24 h insistindo)
Falhas seguidas que desligam o destino20
Ordemem série por destino: o recebido chega antes do clicado

Três consequências práticas:

1. Seu endpoint precisa ser idempotente. O mesmo evento pode chegar mais de uma vez — é o preço de não perder nenhum. Deduplique por X-PM-Entrega-Id.

2. Responda 2xx rápido. Se o seu processamento demora mais de 10 segundos, a tentativa é considerada falha e o evento volta. Aceite, enfileire do seu lado e responda — depois processe.

3. O disjuntor existe para o seu bem. Vinte falhas seguidas do mesmo destino desligam o destino: os eventos ficam esperando na fila em vez de serem queimados contra um servidor que não responde. Consertou o endpoint, você religa pelo painel e a fila drena.

Um endpoint fora do ar por mais de ~24 horas perde eventos por desistência — eles ficam registrados como desistência, auditáveis, nunca apagados em silêncio. Se a sua manutenção for longa, a leitura por janela de tempo (GET /api/v1/notifications/stats) reconstrói os números do período.


Trocando o segredo ou a URL

Reenviar a url gera um segredo novo e desativa o destino anterior. A ordem segura é:

  1. faça o PUT com a URL nova;
  2. guarde o segredo devolvido;
  3. só então aponte o tráfego para o endpoint novo.

Se você trocar a URL e esquecer de atualizar o segredo do lado de lá, todas as entregas passarão a ser rejeitadas como falsas — e a fila vai encher até o disjuntor desligar o destino.


Checklist do endpoint

  • Responde em https://
  • Lê o corpo bruto antes de qualquer desserialização
  • Valida X-PM-Assinatura com comparação em tempo constante
  • Recusa com 401 quando a assinatura não bate
  • Deduplica por X-PM-Entrega-Id
  • Responde 2xx em menos de 10 segundos
  • Guarda o segredo em cofre, nunca no repositório
  • Trata delivery.received, delivery.clicked e notification.completed

Voltando para a consulta

curl -X PUT https://api.pushmesh.io/api/v1/confirmacoes \
  -H "Authorization: Basic $PUSHMESH_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "modo": "consulta" }'

As rotas de consulta voltam a responder na hora. O destino do webhook continua cadastrado — voltar para webhook depois não exige cadastrar de novo (nem gera segredo novo).


Erros da configuração

StatusMotivo
400URL não é https://; modo desconhecido; modo: "webhook" sem destino cadastrado
401chave ausente, inválida ou revogada
403aplicativo pausado
503dependência indisponível — sempre nomeada