Autenticação e erros
O essencial antes de escrever a primeira linha — o formato da chave, a rotação sem queda, o envelope de erro, a tabela de códigos, os limites de borda e por que a API não confirma o que existe.
Autenticação e erros
Esta é a página para ler antes de escrever a primeira linha de integração. Ela responde três perguntas: como você se identifica, o que a API devolve quando algo dá errado, e onde ficam as bordas.
A API pública vive em https://api.pushmesh.io e tem três formas de
identificação. Confundi-las é o erro número um de quem começa:
| Quem chama | O que envia | Rotas |
|---|---|---|
| O seu servidor | Authorization: Basic pm_live_… | envio, consulta, base de aparelhos, In-App, confirmações |
| O aparelho (via SDK ou cliente próprio) | nada — o app_id viaja no corpo | registro de aparelho, recibos, pacote In-App, parâmetros do Firebase |
| Quem opera a plataforma | Authorization: Bearer <chave de plataforma> | criação de aplicativo e rotação por linha de comando |
Existe ainda uma quarta credencial que não é da API: a sessão do painel em
https://app.pushmesh.io. Ela autentica pessoas, dura 7 dias e nunca
substitui a chave do aplicativo — que jamais é enviada ao navegador.
1. A chave do aplicativo
Formato
pm_live_a1b2c3d4_KE7pQ…43caracteres
│ │ └─ segredo: 43 caracteres alfanuméricos
│ └───────────── identificador da chave: 8 caracteres [a-z0-9]
└────────────────────── prefixo: pm_live_ ou pm_test_
A validação é literal. Qualquer desvio de formato nem chega a consultar o
banco: sai 401 na porta.
Onde ela vai
Authorization: Basic pm_live_a1b2c3d4_KE7pQ…
O interpretador do cabeçalho é deliberadamente tolerante, para funcionar com qualquer cliente HTTP. Estas três formas são equivalentes:
# 1. a chave crua depois de Basic (o jeito mais comum)
curl -H "Authorization: Basic $PUSHMESH_KEY" https://api.pushmesh.io/api/v1/confirmacoes
# 2. Bearer também é aceito (maiúsculas/minúsculas não importam)
curl -H "Authorization: Bearer $PUSHMESH_KEY" https://api.pushmesh.io/api/v1/confirmacoes
# 3. base64 de usuario:senha — o que o -u do curl e as bibliotecas clássicas produzem
curl -u "pushmesh:$PUSHMESH_KEY" https://api.pushmesh.io/api/v1/confirmacoes
Na terceira forma o usuário antes dos dois-pontos é ignorado; o que vale é a senha.
O que é guardado
Apenas um resumo criptográfico da chave. A chave em si não fica no banco, nem no log, nem no cache — não existe rota, tela ou pedido de suporte que a mostre de novo. A comparação é feita em tempo constante, de propósito.
pm_test_ é rótulo, não ambiente
Uma chave pode nascer com o prefixo pm_test_, e ele serve para você separar
ambientes na sua configuração. Sendo honesto com você: o prefixo não muda
nada no servidor — a resolução e as permissões são idênticas às da
pm_live_. Não existe um ambiente de sandbox paralelo.
O ensaio de verdade tem outros dois caminhos, e os dois são reais:
- o cabeçalho
X-Dry-Run: trueno envio, que resolve o público e não entrega nem grava; - tokens de aparelho começando com
test:, que registram normalmente e ficam fora de qualquer disparo.
O app_id tem que ser o da chave
Quase toda rota autenticada exige o app_id (na query ou no corpo) além do
cabeçalho. Se ele não for o aplicativo daquela chave, a resposta é 401 — não
404, e não 403. O motivo está na seção 7.
2. Rotacionar sem derrubar a integração
Rotacionar emite uma chave nova e mantém a anterior válida por 24 horas. É uma janela de convivência: nenhuma chamada em voo quebra, e você não precisa de uma janela de manutenção.
A resposta da rotação traz a chave nova (uma única vez) e o momento em que a anterior expira:
{
"id": "00000000-0000-0000-0000-000000000000",
"api_key": "pm_live_e5f6g7h8_novo_segredo_de_43_caracteres",
"kid": "e5f6g7h8",
"anterior_expira_em": "2026-08-28T14:05:00Z"
}
Sequência recomendada:
- rotacione (no painel ou pela rota de plataforma);
- guarde a chave nova no seu cofre de segredos;
- reinicie/atualize os serviços que usam a chave, com folga dentro das 24 h;
- confirme que nada mais usa a antiga antes do prazo.
Duas honestidades sobre o prazo:
- A chave anterior continua funcionando durante a janela inteira. Rotação é troca sem queda, não corte imediato de acesso: se a sua chave vazou e você precisa invalidá-la agora, rotacionar não basta — fale com a plataforma.
- A resolução de credencial é cacheada por 60 segundos. Na prática, uma chave já vencida ainda pode passar por até cerca de um minuto além do prazo, em instâncias que já a tinham em memória. É o pior caso, e vale a pena conhecê-lo antes de cronometrar um corte no relógio.
Depois da janela, a chave antiga responde:
{
"errors": ["não autorizado"],
"explain": {
"causa": "chave inexistente, revogada ou expirada",
"como_corrigir": "confira a chave; se houve rotação, a anterior vale por 24h",
"request_id": "0198f0b2-6e31-7a4c-9f10-2c9a1d4e7b55"
}
}
3. As rotas que não pedem chave — e por que
Estas rodam dentro do aplicativo instalado no celular e não levam
Authorization:
- registro e atualização de aparelho;
- recibo de entrega e de clique;
- pacote e eventos de mensagens In-App;
- parâmetros públicos do Firebase.
Isso não é um esquecimento; é o desenho. Um aplicativo publicado é um arquivo que qualquer pessoa baixa e inspeciona — uma chave embutida nele é uma chave publicada. O que protege essas rotas no lugar da credencial:
| Proteção | O que faz |
|---|---|
| Validação do aplicativo | o app_id do corpo é conferido antes de qualquer acesso a dado: inexistente ⇒ 400, pausado ⇒ 403. |
| Validação de forma | cada campo é checado antes de tocar o banco; pedido malformado nunca custa banco. |
| Teto por origem | 120 requisições por minuto por endereço de origem, com Retry-After: 60. |
| Prova criptográfica | recibos e eventos In-App só contam com a prova que o próprio servidor injetou naquele envio, verificada em tempo constante. |
Regra prática: pm_live_ só existe no seu servidor. Se você precisa
consultar um aparelho a partir do backend, use a rota de consulta autenticada
— nunca leve a chave para dentro do aplicativo.
4. A chave de plataforma
Duas rotas — criar aplicativo e rotacionar chave — aceitam
Authorization: Bearer <chave de plataforma>. Ela pertence a quem opera o
serviço, não ao cliente: no uso normal, você cria o aplicativo e rotaciona a
chave pelo painel, que faz exatamente a mesma coisa (inclusive a janela de
24 h).
Três detalhes que economizam depuração:
- a credencial é conferida antes do corpo. JSON quebrado sem credencial
responde
401, não400— quem depura pelo corpo procura no lugar errado; - se a instância não tem essa chave configurada, a resposta é
503nomeando a configuração ausente, e ela vem antes do401; - toda tentativa recusada é registrada guardando, no máximo, o identificador da chave — nunca a chave apresentada.
5. O envelope de erro
Um erro que só diz “400 Bad Request” transfere o trabalho para você. Aqui, a resposta de erro é parte do produto: ela diz o que aconteceu, o que fazer agora e carrega o identificador da requisição.
100% dos 4xx/5xx sai neste formato:
{
"errors": ["requisição inválida: contents é obrigatório (objeto {lang: texto})"],
"explain": {
"causa": "contents é obrigatório (objeto {lang: texto})",
"como_corrigir": "corrija o payload; retry cego não resolve",
"request_id": "0198f0b2-6e31-7a4c-9f10-2c9a1d4e7b55",
"doc_url": "https://api.pushmesh.io/docs#erros"
}
}
| Campo | O que é |
|---|---|
errors | array de strings. É o campo que integrações clássicas já leem — mantido por compatibilidade. |
explain.causa | o que de fato aconteceu, em uma frase. |
explain.como_corrigir | a ação seguinte. Quando repetir não resolve, ele diz isso. |
explain.request_id | identificador desta requisição. Vem também no cabeçalho X-Pm-Request-Id, inclusive nas respostas de sucesso. |
explain.doc_url | âncora na documentação. Só aparece quando a instância tem endereço público configurado. |
Alguns erros trazem os números que faltavam
O explain é aditivo. Chave de idempotência reusada com corpo diferente, por
exemplo, devolve os dois resumos para você comparar:
{
"errors": ["idempotency_key reused with a different request body"],
"explain": {
"causa": "request_hash divergente",
"como_corrigir": "use uma Idempotency-Key nova para um conteúdo novo",
"hash_original": "3f2a…",
"hash_recebido": "9b71…",
"request_id": "0198f0b2-6e31-7a4c-9f10-2c9a1d4e7b55"
}
}
E o payload acima do orçamento diz quantos bytes são seus, quantos o servidor injeta e quantos encurtar:
{
"errors": ["payload renderizado acima do limite: 4210 bytes (máximo 3891)"],
"explain": {
"causa": "o payload final FCM/APNs tem 4210 bytes — 4114 da sua mensagem renderizada mais 96 que o servidor injeta no envio (pm_msg_id + pm_rcpt, o rastro que permite recibo de entrega e de clique); o orçamento é 3891 (3,8 KB)",
"como_corrigir": "encurte contents/headings/data/imagens em pelo menos 319 bytes — o limite é do provedor (4 KB), com margem",
"bytes": 4210,
"limite": 3891,
"bytes_injetados_pelo_servidor": 96
}
}
Duas ressalvas de leitura
errorspode ser objeto — mas só num200. No envio parcialmente resolvido ele vem como{"invalid_player_ids": [...]}, e a chamada é sucesso. Se você desserializa para um tipo fixo, aceite a uniãoarray | objeto.- Os textos de
errorseexplainestão em português, sem negociação de idioma. Ramifique o seu código pelo código HTTP e pelos campos estruturados doexplain, nunca pela frase — ela é prosa para humano e pode ser melhorada a qualquer momento.
6. Tabela de códigos
| Código | O que significa aqui |
|---|---|
| 200 | sucesso. Inclui dois casos que parecem erro: ninguém alcançável (id vazio, recipients: 0, errors array) e envio parcial (errors objeto). |
| 201 | aplicativo criado. |
| 304 | o pacote In-App não mudou desde o ETag que você apresentou. |
| 400 | a forma do pedido está errada: campo obrigatório ausente, tipo errado, valor fora da faixa, campo não suportado. Repetir não resolve. |
| 401 | problema de credencial: ausente, formato desconhecido, inexistente, revogada, expirada — ou app_id que não é o da chave. |
| 403 | credencial válida, permissão não: aplicativo pausado, canal In-App desligado, modo de confirmação incompatível com a rota, prova de recibo inválida. |
| 404 | o recurso não existe dentro do escopo desta chave. |
| 409 | conflito de estado: outra chamada com a mesma chave de idempotência está processando agora; ou uma campanha In-App em estado que não aceita a operação. |
| 413 | corpo acima do limite da rota. A causa nomeia o limite. |
| 422 | forma válida, conteúdo impossível: chave de idempotência reusada com corpo diferente, payload acima de 3.891 bytes, nome reservado em data, campanha In-App inválida — e a conta no plano gratuito acima da franquia de usuários ativos do mês, caso em que o envio é recusado inteiro (seção 8). |
| 429 | teto estourado (por origem nas rotas do aparelho; por aplicativo nas classes de envio, consulta e exportação). |
| 500 | falha interna. Repetir com a mesma chave de idempotência é seguro por contrato. |
| 503 | dependência de configuração ou de infraestrutura ausente. A mensagem nomeia o que falta, sem jamais revelar segredo. |
7. Por que a API não confirma o que existe
Esta é a parte que costuma surpreender — e é justamente a que dá confiança a quem entende de segurança.
A API não é oráculo de existência. Se ela respondesse 404 quando a chave
é de outro aplicativo, qualquer pessoa com uma chave válida poderia varrer
identificadores e descobrir o que existe na plataforma só pela diferença entre
401 e 404. Por isso:
401é sobre a chave. Inclui o caso “sua chave é boa, mas você pediu umapp_idque não é o dela”. Do ponto de vista daquela chave, aquele aplicativo simplesmente não é acessível.403é sobre permissão, com a identidade já estabelecida. Aplicativo pausado, canal In-App desligado, ou a rota de consulta num aplicativo que escolheu receber confirmações por webhook — porque confirmação é um canal ou outro, nunca os dois.404é sobre o conteúdo do seu próprio escopo. Como todo acesso a dado passa por um escopo de cliente antes de tocar o banco, um recurso de outro cliente é indistinguível de um recurso inexistente. É o mesmo404, de propósito.
E a mesma lógica vale para as provas criptográficas dos recibos: o 403 de
prova inválida tem sempre o mesmo texto, nunca revela o valor esperado, e é
avaliado antes de qualquer consulta de existência — senão a rota viraria um
oráculo para forjar a prova por tentativa e erro.
{
"errors": ["proibido"],
"explain": {
"causa": "prova de recibo (rcpt) inválida",
"como_corrigir": "não construa o rcpt no cliente: ele vem pronto no payload do push (data.pm_rcpt) e deve ser ecoado como veio"
}
}
8. As bordas
Tamanho do corpo
| Rotas | Limite | O que acontece ao passar |
|---|---|---|
| Rotas do aparelho (registro, recibo, eventos In-App) | 16 KB | 413. Se o Content-Length já declara excesso, a resposta sai sem ler o corpo. |
| Envio de notificação | 256 KB | 413. O limite é maior porque aqui viaja a lista de destinatários (2.000 identificadores ocupam cerca de 80 KB). |
O 413 sai no mesmo envelope dos outros erros, com o limite nomeado na causa —
nunca um 413 cru de servidor intermediário.
Requisições
| Teto | Onde vale | Ao estourar |
|---|---|---|
| 120 por minuto, por origem | rotas do aparelho | 429 com Retry-After: 60. Requisições recusadas também contam na janela. |
| 6.000 por segundo, por aplicativo | envio de notificação | 429, cobrado antes de qualquer leitura de dados. |
| 1.000 por segundo, por aplicativo | consulta do resultado de um disparo | 429. Só conta o que vai ao banco: perguntar o mesmo disparo repetidamente é servido de um cache de 1 segundo e não entra na conta. |
| 1 por segundo, por aplicativo | exportação da base de aparelhos | 429. O arquivo anterior continua valendo — baixe a URL que você já recebeu. |
| 50.000 registros de aparelho por hora, por aplicativo | registro de aparelho | 429, com a janela de uma hora. |
| 10 aparelhos por identificador de usuário | registro e vínculo de aparelho | 400 nomeado. |
Honestidade sobre o
Retry-After: o texto do429por aplicativo menciona esse cabeçalho, mas só o429por origem (rotas do aparelho) o emite de fato. A regra segura para o seu cliente: honreRetry-Afterquando ele existir; na ausência, espere o valor citado emexplain.como_corrigir(nos tetos por aplicativo, 1 segundo).
Conteúdo da mensagem
O payload final entregue ao provedor não pode passar de 3.891 bytes. Esse orçamento inclui 96 bytes que o servidor injeta depois da sua chamada — o identificador do disparo e a prova de recibo, isto é, exatamente o que permite medir a entrega real. A verificação acontece na porta, antes de qualquer gravação, e o erro diz quantos bytes encurtar.
A borda comercial: a franquia do plano gratuito
Esta é a única borda desta página que não é técnica — e a única que recusa o
envio inteiro. Ela merece um parágrafo porque surpreende quem trata 422 como
“pedido malformado” e desiste.
O plano gratuito entrega dentro de uma franquia de usuários ativos no mês.
Acima dela, o POST /api/v1/notifications responde 422 e nada é enviado —
nem para uma parte da lista. Truncar seria pior: você veria “campanha enviada” e
parte da sua base simplesmente não receberia, sem sinal nenhum.
A recusa é escrita para você agir sem abrir chamado. O explain vem com os
números:
{
"errors": ["plano Free atende até 1000 usuários ativos no mês — sua conta está com 1240"],
"explain": {
"causa": "o plano Free entrega dentro da franquia de 1000 usuários ativos no mês, e em 2026-08 a sua conta registrou 1240; acima da franquia o envio é recusado INTEIRO — entregar só para uma parte da base, escolhida por ordem de consulta, seria sumir com o resto em silêncio",
"como_corrigir": "mude para o Premium (USD 0.005 por usuário ativo acima da franquia, sem mensalidade) e o envio volta na hora — os 1000 primeiros usuários ativos continuam gratuitos nos dois planos",
"plano": "free",
"mau": 1240,
"mau_gratis": 1000,
"mes": "2026-08",
"moeda": "USD",
"preco_por_mau": "0.005",
"medido_ha_s": 12
}
}
Campo do explain | O que é |
|---|---|
plano | o plano da conta hoje |
mau | usuários ativos medidos no mês |
mau_gratis | a franquia |
mes | o mês medido, em AAAA-MM |
moeda, preco_por_mau | o preço vigente por usuário ativo acima da franquia |
medido_ha_s | a idade da medição, em segundos |
Quatro coisas que valem saber antes de escrever o tratamento desse erro:
- Repetir não resolve. É
422justamente por isso, e não429: esperar não muda o número. A correção é mudar de plano. - O número não é tempo real. A contagem varre a base, então é refeita
periodicamente em segundo plano — no máximo uma vez por minuto — e
medido_ha_sdiz com que idade de número você está discutindo. - Conta em plano pago nunca é recusada aqui. Ela não entra na medição, então não existe caminho de código que a barre.
- Falha de infraestrutura não barra ninguém. Sem banco, ou com uma medição velha demais (mais de 600 segundos), a conferência libera o envio. Errar para o lado de entregar é decisão escrita.
O valor da franquia vive na configuração de cobrança e a página de preços é a fonte da verdade; os números do exemplo acima são ilustrativos. O resumo de todos os tetos está em Limites.
9. Repetir ou não repetir
| Código | Repetir a mesma chamada? |
|---|---|
| 400, 422 | Não. Corrija o pedido; o explain diz o campo. No 422 da franquia do plano gratuito, a correção não é no seu JSON — é no plano. |
| 401, 403 | Não. Corrija a credencial ou a configuração do aplicativo. |
| 404 | Não. Confira o identificador. |
| 409 | Sim, depois de esperar alguns segundos — ou leia o resultado do disparo. |
| 413 | Não sem encolher o corpo ou dividir o lote. |
| 429 | Sim, respeitando a espera. |
| 500, 503 | Sim, com a mesma chave de idempotência — é seguro por contrato. |
Sobre a chave de idempotência, o mínimo que você precisa saber agora: mande
Idempotency-Key no envio, guarde-a, e reenvie a mesma chave com o mesmo
corpo em caso de falha de rede ou 5xx. A resposta original volta idêntica
(com o cabeçalho X-Idempotent-Replay: true) por 30 dias, e a mesma chave
nunca dispara duas campanhas. Mesma chave com corpo diferente é 422, de
propósito. O contrato completo está em
Enviar push.
Guarde o request_id das falhas. É ele que liga a sua chamada ao rastro do
serviço, e é a primeira coisa que o suporte vai pedir.
Veja também
| Assunto | Página |
|---|---|
| do zero ao primeiro push com recibo | Primeiros passos |
| todos os cabeçalhos aceitos e emitidos | Cabeçalhos |
| a tabela completa de tetos e limites de produto | Limites |
| criar o aplicativo e rotacionar a chave | Aplicativos e chaves |
| confirmações assinadas em vez de consulta | Webhooks |
| o que é congelado por contrato e o que pode mudar | Versionamento |