API Mágica: notificações Web Push no navegador com PHP
Olá meus Unicórnios! 🦄✨
Sabe aquela notificação que aparece no canto da tela avisando que saiu artigo novo, que o pedido foi enviado, que chegou mensagem? Mesmo com a aba do site fechada? Isso é Web Push. E a API Mágica ganhou um recurso só para ele. 🔔
Quem já tentou fazer Web Push do zero sabe onde dói: gerar o par de chaves VAPID, cifrar cada mensagem para cada navegador, assinar a requisição, descobrir quais inscrições morreram e apagar. É criptografia de verdade, e errar um byte faz a notificação sumir sem erro nenhum. 😅
A API Mágica fica com essa parte inteira. Ela guarda as inscrições, cifra e envia as notificações, desativa sozinha as inscrições expiradas e ainda conta os cliques. Do lado do seu site sobra pouco: um service worker, um JavaScript que pede a permissão, e um PHP que repassa a inscrição. Neste tutorial eu monto um site de exemplo em PHP puro, sem Composer e sem biblioteca, e mostro cada pedaço funcionando. ✨
Se você quer o código antes da explicação, ele está inteiro aqui:
🧭 Como as peças conversam
Antes do código, vale entender quem fala com quem, porque é aqui que mora a regra mais importante do tutorial inteiro:
navegador (app.js) ──inscrição──> ponte.php ──X-API-Key──> API Mágica
(seu servidor) │
│ cifra e envia
▼
sw.js mostra a notificação <──── serviço de push do navegador
(Google, Mozilla, Apple, Microsoft)
Repare que o navegador nunca chama a API Mágica. Toda chamada para ela leva a sua chave no cabeçalho X-API-Key, e tudo que vai para o navegador qualquer visitante consegue ler (é só apertar F12). Por isso existe a ponte: um arquivo PHP no seu servidor que recebe a inscrição do navegador e repassa para a API, com a chave que só ele conhece. 🔐
O site de exemplo tem estes arquivos, todos pequenos:
apimagica_webpush/
├── config.exemplo.php modelo do config.php (a chave mora nele)
├── api.php a única função que fala com a API
├── criar-projeto.php roda uma vez, no terminal
├── index.php a página com o botão e o formulário
├── app.js pede a permissão e inscreve o navegador
├── sw.js o service worker: mostra a notificação
├── ponte.php repassa a inscrição para a API
├── enviar.php dispara a notificação
└── estilo.css
🔑 A chave e o config.php
Primeiro, a chave. Ela sai do painel da API Mágica: você se cadastra, gera a chave e copia. É de graça, sem cartão.
No repositório vem um config.exemplo.php. Faça uma cópia dele com o nome config.php (no Windows, copy config.exemplo.php config.php; no Linux e no Mac, cp config.exemplo.php config.php) e cole a sua chave no lugar de SUA_CHAVE_AQUI:
<?php
// Copie este arquivo para config.php e preencha.
// O config.php está no .gitignore: a sua chave nunca sobe para o GitHub.
// A sua chave, gerada no painel da API Mágica.
$API_CHAVE = 'SUA_CHAVE_AQUI';
// Preencha depois de rodar, no terminal: php criar-projeto.php
$PROJETO_ID = 0;
$CHAVE_PUBLICA = '';
O config.php está no .gitignore. É esse detalhe que impede a chave de subir para o GitHub junto com o resto do código, e é por isso que o repositório traz um arquivo "exemplo" em vez do arquivo de verdade. As outras duas variáveis ficam vazias por enquanto: quem preenche é o próximo passo.
🧰 A única função que fala com a API
Todo o PHP do exemplo conversa com a API Mágica por uma função só, a chamarApi(). Ela usa o cURL, que já vem no PHP (só confira se a extensão curl está ligada no seu php.ini):
<?php
require __DIR__ . '/config.php';
$API_URL = 'https://api.apimagica.com.br/v1';
// Chama a API Mágica e devolve o status HTTP e a resposta já decodificada.
function chamarApi($metodo, $caminho, $dados = null)
{
global $API_URL, $API_CHAVE;
$curl = curl_init($API_URL . $caminho);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $metodo);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'X-API-Key: ' . $API_CHAVE,
));
if ($dados !== null) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($dados));
}
$resposta = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$erro = curl_error($curl);
curl_close($curl);
if ($resposta === false) {
return array('status' => 0, 'dados' => array('erro' => 'Sem conexão com a API: ' . $erro));
}
return array('status' => $status, 'dados' => json_decode($resposta, true));
}
Ela devolve sempre as mesmas duas coisas: o status HTTP e os dados da resposta, já convertidos para array. Quem chama decide o que fazer com cada status. E quando a rede cai, ela não explode: devolve status 0 com uma mensagem em português dizendo o que houve.
🏗️ Criando o projeto de Web Push
Na API Mágica, tudo do Web Push vive dentro de um projeto: um por site. É o projeto que guarda o par de chaves VAPID (a "identidade" do seu servidor perante os serviços de push), as inscrições e os envios. Você cria uma vez só, com este script:
<?php
// Rode uma vez só, no terminal: php criar-projeto.php
// Cria o projeto de Web Push na API Mágica e mostra o que vai no config.php.
require __DIR__ . '/api.php';
$resposta = chamarApi('POST', '/webpush/projetos', array(
'nome' => 'Meu site de teste',
'site_url' => 'http://localhost:8000',
'subject' => '[email protected]', // e-mail de contato, exigido pelo VAPID
));
if ($resposta['status'] !== 201) {
echo "A API recusou (HTTP {$resposta['status']}):\n";
echo json_encode($resposta['dados'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
exit(1);
}
$projeto = $resposta['dados'];
echo "Projeto criado! Copie estas duas linhas para o config.php:\n\n";
echo "\$PROJETO_ID = {$projeto['id']};\n";
echo "\$CHAVE_PUBLICA = '{$projeto['chave_publica_vapid']}';\n";
Três campos, e cada um tem sua razão de ser:
nome: só para você se achar no painel.site_url: o endereço do site. Precisa dehttps://, com uma exceção:http://localhosté aceito, justamente para testar na sua máquina. É também o endereço que abre quando alguém clica numa notificação sem link próprio.subject: um e-mail de contato. O protocolo VAPID exige, para que o serviço de push saiba com quem falar se o seu servidor começar a se comportar mal.
Rode no terminal, dentro da pasta do projeto, com php criar-projeto.php. Se você esquecer de trocar o SUA_CHAVE_AQUI (eu esqueci 🙈), a resposta é esta:
A API recusou (HTTP 401):
{
"erro": {
"codigo": "API_KEY_INVALIDA",
"mensagem": "Chave invalida ou revogada.",
"detalhes": []
}
}
Com a chave certa, o projeto nasce e o script imprime exatamente as duas linhas que faltam no config.php:
Projeto criado! Copie estas duas linhas para o config.php:
$PROJETO_ID = 2;
$CHAVE_PUBLICA = 'BIdij1Tr8jppy5mOGIII06...(encurtei)...wFkqfJUOzc';
(Encurtei a chave aqui no artigo; a sua vem com 87 caracteres.) Essa é a chave pública VAPID, e ela pode ir para o navegador sem problema nenhum: é pública de propósito. A privada, a que assina os envios, a API gera junto e nunca devolve. Ela fica guardada lá, e é por isso que você não precisa de biblioteca de criptografia nenhuma. 🪄
👷 O service worker, quem recebe o push
O service worker é um JavaScript que o navegador guarda e roda por trás da página, e que continua vivo mesmo com a aba fechada. É ele quem recebe o push e mostra a notificação. No exemplo, é o sw.js, e ele fica na raiz do site:
// O service worker roda por trás da página e continua vivo com a aba fechada.
// É ele quem recebe o push e mostra a notificação.
self.addEventListener('push', function (evento) {
// A API Mágica manda: { titulo, corpo, url, icone, clique }
const dados = evento.data.json();
// Sem o waitUntil, o navegador pode desligar o service worker
// antes de a notificação aparecer.
evento.waitUntil(
self.registration.showNotification(dados.titulo, {
body: dados.corpo,
icon: dados.icone,
data: { url: dados.url, clique: dados.clique },
})
);
});
self.addEventListener('notificationclick', function (evento) {
const dados = evento.notification.data;
evento.notification.close();
// Avisa a ponte do clique (a API conta) e abre a página da notificação.
const avisarClique = fetch('/ponte.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ acao: 'clique', token: dados.clique }),
});
const abrirPagina = self.clients.openWindow(dados.url);
evento.waitUntil(Promise.all([avisarClique, abrirPagina]));
});
Dois eventos, dois trabalhos:
push: chega a mensagem que a API Mágica mandou. Ela vem em JSON, com os campos em português (titulo,corpo,url,icone) e um campoclique, que é um token para contar o clique depois. O service worker só traduz isso para oshowNotification().notificationclick: alguém clicou. O service worker fecha a notificação, abre a página daurle manda o token para a ponte, que repassa para a API contar.
Repare no evento.waitUntil() nos dois. Ele diz ao navegador "espere eu terminar". O navegador desliga o service worker assim que acha que ele não tem mais nada para fazer, e sem o waitUntil ele pode desligar antes de a notificação aparecer ou antes de o clique ser avisado.
🙋 Pedindo permissão e inscrevendo o navegador
Agora a página. O botão carrega a chave pública num atributo data-chave, lida do config.php:
<button id="botao-ativar" data-chave="<?= htmlspecialchars($CHAVE_PUBLICA) ?>">
Quero receber notificações
</button>
<p id="situacao"></p>
E o app.js faz o resto, em quatro passos numerados:
// A chave pública vem em texto (base64url); o navegador quer os bytes.
function chaveParaBytes(texto) {
const base64 = (texto + '==='.slice((texto.length + 3) % 4))
.replace(/-/g, '+')
.replace(/_/g, '/');
const bruto = atob(base64);
const bytes = new Uint8Array(bruto.length);
for (let i = 0; i < bruto.length; i++) {
bytes[i] = bruto.charCodeAt(i);
}
return bytes;
}
async function ativarNotificacoes() {
const botao = document.getElementById('botao-ativar');
const situacao = document.getElementById('situacao');
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
situacao.textContent = 'Este navegador não suporta notificações push.';
return;
}
// 1. Pede a permissão. Sempre dentro de um clique: pedir sozinho, ao abrir
// a página, alguns navegadores (o Firefox, por exemplo) recusam.
const permissao = await Notification.requestPermission();
if (permissao !== 'granted') {
situacao.textContent = 'Você não permitiu as notificações.';
return;
}
// 2. Registra o service worker e espera ele ficar ativo.
// Inscrever antes disso dá erro: "no active Service Worker".
await navigator.serviceWorker.register('/sw.js');
const registro = await navigator.serviceWorker.ready;
// 3. Inscreve no serviço de push do navegador (Google, Mozilla, Apple...).
situacao.textContent = 'Inscrevendo...';
const inscricao = await registro.pushManager.subscribe({
// Obrigatório. Com false, o Chrome recusa dizendo 'permission denied',
// mesmo com a permissão concedida.
userVisibleOnly: true,
applicationServerKey: chaveParaBytes(botao.dataset.chave),
});
// 4. Entrega a inscrição para a ponte, no NOSSO servidor.
const resposta = await fetch('/ponte.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ acao: 'inscrever', inscricao: inscricao }),
});
const resultado = await resposta.json();
if (resultado.ok) {
situacao.textContent = 'Pronto! Este navegador vai receber as notificações.';
} else {
situacao.textContent = 'A ponte não conseguiu registrar a inscrição.';
}
}
// O .catch mostra o erro na tela. Sem ele, uma falha dentro da função
// async some em silêncio e o botão parece não fazer nada.
document.getElementById('botao-ativar').addEventListener('click', function () {
ativarNotificacoes().catch(function (erro) {
document.getElementById('situacao').textContent = 'Deu erro: ' + erro.message;
});
});
Parece tudo muito direto, mas três linhas desse arquivo existem porque algo deu errado sem elas. Se você só for ler um pedaço deste artigo, leia este. 🙏
1. O ready depois do register. O register() só registra o service worker; ele ainda vai instalar e ativar. Quem espera ele ficar pronto é o navigator.serviceWorker.ready. Tentei inscrever direto no registro recém-criado, e o Chrome respondeu:
AbortError: Failed to execute 'subscribe' on 'PushManager': Subscription failed - no active Service Worker
2. O userVisibleOnly: true. É a sua promessa de que todo push vai mostrar uma notificação (nada de push "escondido" rodando código no computador da pessoa). O Chrome não aceita outro valor. E aqui vem o detalhe cruel: com false, a mensagem de erro fala de permissão, com a permissão concedida e tudo:
NotAllowedError: Registration failed - permission denied
Você vai olhar a permissão, ver "Permitir", e não entender nada. 🤯 Agora você já sabe onde procurar.
3. O .catch no fim. A ativarNotificacoes() é async, e erro dentro de função async que ninguém captura some em silêncio: o botão simplesmente não faz nada. O .catch pega qualquer falha e escreve na tela, e foi com ele que as duas mensagens acima apareceram para mim.
A conversão chaveParaBytes() também tem motivo: a API entrega a chave pública em texto base64url, e o subscribe() quer os bytes num Uint8Array. As duas trocas (- por + e _ por /) e os = do fim transformam base64url em base64 comum, que o atob() sabe ler.
Para ver funcionando, suba o site com o servidor embutido do PHP:
php -S localhost:8000
E abra http://localhost:8000. Use localhost mesmo: service worker só funciona em HTTPS ou em localhost, que o navegador trata como endereço seguro. Clique no botão, aceite a permissão, e o recado aparece:
Uma coisa que me pegou: num perfil novinho do Chrome, a primeira inscrição passou de 40 segundos. O navegador ainda estava se conectando ao serviço de push do Google. Foi por isso que entrou o "Inscrevendo..." antes do subscribe(): sem ele, a pessoa acha que o botão travou e clica de novo.
🌉 A ponte, onde a chave fica guardada
O app.js entrega a inscrição para a ponte.php. A inscrição é um objeto com o endpoint (o endereço do navegador no serviço de push) e duas chaves, p256dh e auth, que a API usa para cifrar a mensagem de um jeito que só aquele navegador consegue abrir. A ponte repassa isso com a chave da API:
<?php
// A ponte: o navegador fala com ela, e ela fala com a API Mágica.
// Assim a chave da API fica aqui no servidor e nunca vai para o navegador.
require __DIR__ . '/api.php';
$corpo = json_decode(file_get_contents('php://input'), true);
if (isset($corpo['acao']) && $corpo['acao'] === 'inscrever') {
// O navegador manda a inscrição inteira; a API só precisa destes dois campos.
$resposta = chamarApi('POST', "/webpush/projetos/$PROJETO_ID/inscricoes", array(
'endpoint' => $corpo['inscricao']['endpoint'],
'keys' => $corpo['inscricao']['keys'],
));
} elseif (isset($corpo['acao']) && $corpo['acao'] === 'clique') {
$resposta = chamarApi('POST', "/webpush/projetos/$PROJETO_ID/cliques", array(
'token' => $corpo['token'],
));
} else {
http_response_code(400);
exit('Ação desconhecida.');
}
// O navegador só fica sabendo se deu certo. O resto da resposta fica aqui.
header('Content-Type: application/json');
$deuCerto = $resposta['status'] >= 200 && $resposta['status'] < 300;
echo json_encode(array('ok' => $deuCerto));
Ela cuida de duas ações: inscrever, que vem do app.js, e clique, que vem do sw.js. E devolve ao navegador só {"ok": true} ou {"ok": false}. A resposta completa da API fica no servidor: o navegador não precisa saber o id da inscrição, nem nada do projeto.
E se alguém mal-intencionado mandar para a sua ponte um endpoint inventado, apontando para um servidor qualquer? A API recusa. Ela só aceita endereços dos serviços de push conhecidos:
{
"erro": {
"codigo": "ENDPOINT_NAO_PERMITIDO",
"mensagem": "O endpoint nao pertence a um servico de push conhecido (Google, Mozilla, Apple ou Microsoft).",
"detalhes": []
}
}
Mandar a mesma inscrição duas vezes também não é problema: a API identifica a inscrição pelo endpoint e atualiza a existente em vez de criar outra. Então o visitante pode clicar no botão quantas vezes quiser.
🚀 Enviando a notificação
Com o navegador inscrito, falta disparar. É o enviar.php, que recebe o formulário da página:
<?php
// Dispara a notificação para todos os inscritos do projeto.
// ATENÇÃO: num site de verdade, esta página fica atrás do login do
// administrador. Do jeito que está, qualquer visitante enviaria para todos.
require __DIR__ . '/api.php';
if (isset($_GET['id'])) {
// Consulta como ficou um envio que já foi feito.
// O id é texto (env_...): com (int), ele vira 0 e a API responde 404.
$resposta = chamarApi('GET', "/webpush/projetos/$PROJETO_ID/envios/" . rawurlencode($_GET['id']));
} else {
$resposta = chamarApi('POST', "/webpush/projetos/$PROJETO_ID/envios", array(
'destino' => array('todos' => true),
'notificacao' => array(
'titulo' => $_POST['titulo'],
'corpo' => $_POST['corpo'],
),
));
}
$envio = $resposta['dados'];
$json = json_encode($envio, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
O destino {"todos": true} manda para todas as inscrições ativas do projeto. Existem outras formas, uma de cada vez: por tags, por ids_externos ou por uma lista de inscricoes. Na notificacao, só o titulo é obrigatório. Sem url, vale o site_url do projeto.
Leia o comentário ATENÇÃO do topo com carinho. Deixei o enviar.php aberto para o exemplo ficar simples, mas num site de verdade ele fica atrás do login do administrador. Do jeito que está, qualquer visitante mandaria notificação para todos os seus inscritos. 😱
A API responde 202 na hora, e não 200. O 202 quer dizer "recebi, vou processar": o envio acontece em segundo plano, e é por isso que ele volta pendente e com os contadores zerados:
{
"id": "env_BhLu_7PQGpYAJApa",
"estado": "pendente",
"destino": {
"todos": true
},
"notificacao": {
"url": "http://localhost:8000",
"corpo": "Esta notificação veio da API Mágica.",
"titulo": "Olá, unicórnio! 🦄"
},
"ttl": 86400,
"urgencia": "normal",
"agendado_para": null,
"contadores": {
"total": null,
"aceitos": 0,
"falhas": 0,
"removidas": 0,
"cliques": 0
},
"ultimo_erro": null,
"criado_em": "2026-09-24T17:20:45.373Z",
"iniciado_em": null,
"concluido_em": null
}
Uns dois segundos depois, a notificação aparece no canto da tela:
🐛 O bug do id que virava zero
A página de resposta tem um link "Ver como ficou este envio", que consulta o envio pelo id. Na primeira versão, eu escrevi assim:
$resposta = chamarApi('GET', "/webpush/projetos/$PROJETO_ID/envios/" . (int) $_GET['id']);
O (int) estava ali por costume: id que chega pela URL, a gente converte para número e fica tranquila. Só que eu nem olhei o formato do id. E a API respondeu:
{
"erro": {
"codigo": "ENVIO_NAO_ENCONTRADO",
"mensagem": "O envio 0 nao existe neste projeto.",
"detalhes": []
}
}
"O envio 0". 😳 O id do envio não é número: é texto, como env_BhLu_7PQGpYAJApa. E o (int) do PHP, diante de um texto que não começa com dígito, devolve 0 sem reclamar. O bug era meu, não da API. A versão certa usa rawurlencode(), que protege a URL sem mudar o valor, e o comentário ficou no código para ninguém "consertar" de volta.
Com o id certo, a consulta mostra o envio concluído. Este é de um disparo em que eu cliquei na notificação:
{
"id": "env_G3YMZMdYZIeJIbbM",
"estado": "concluido",
"destino": {
"todos": true
},
"notificacao": {
"url": "http://localhost:8000",
"corpo": "Esta notificação veio da API Mágica.",
"titulo": "Olá, unicórnio! 🦄"
},
"ttl": 86400,
"urgencia": "normal",
"agendado_para": null,
"contadores": {
"total": 1,
"aceitos": 1,
"falhas": 0,
"removidas": 0,
"cliques": 1
},
"ultimo_erro": null,
"criado_em": "2026-09-24T17:24:52.568Z",
"iniciado_em": "2026-09-24T17:24:54.000Z",
"concluido_em": "2026-09-24T17:24:55.000Z"
}
Os contadores merecem uma leitura atenta:
aceitosconta o que o serviço de push do navegador aceitou. Ele não confirma que a notificação foi exibida: se a pessoa estiver com o "não perturbe" ligado, o push é aceito do mesmo jeito.cliquesé o sinal de que alguém viu. Ele vem do token que osw.jsmanda para a ponte. A API conta um clique por inscrição por envio, então clicar duas vezes não infla o número.removidassão as inscrições que o serviço de push declarou mortas (o navegador foi desinstalado, a pessoa limpou os dados). A API desativa essas sozinha, e o próximo envio já nem tenta.
🩺 Quando a notificação não chega
A API tem uma rota feita para essa hora, o GET /v1/webpush/projetos/{id}/diagnostico. Com a chamarApi(), é uma linha:
$resposta = chamarApi('GET', "/webpush/projetos/$PROJETO_ID/diagnostico");
A resposta começa com os dados do projeto (recortei essa parte) e depois traz o que interessa:
{
"inscricoes": {
"ativas": 1,
"inativas": 1,
"por_servico": {
"google": 1
}
},
"envios": {
"criados_ultimas_24h": 5,
"cota_diaria": 20,
"agendados_pendentes": 0,
"em_andamento": 0
},
"ultimos_erros": [],
"alertas": []
}
Essa inscrição inativa tem história: é de um subscribe() que eu disparei duas vezes seguidas enquanto o primeiro ainda estava pendurado. O Chrome ficou com a segunda inscrição, a primeira morreu, e a API a desativou sozinha no envio seguinte. Nada para limpar à mão.
Olhe principalmente três coisas:
ativasem zero: a inscrição nunca chegou. O problema está noapp.jsou na ponte, não no envio.criados_ultimas_24hperto dacota_diaria: são 20 envios por projeto a cada 24 horas. Um envio para "todos" conta como um, não importa quantos inscritos existam.alertas: quando a API percebe algo errado (nenhuma inscrição ativa, cota quase no fim), o aviso aparece aqui.
E lembre do limite de requisições da conta, de 5 por minuto por endpoint. As rotas que a ponte usa (inscrever e clique) têm um limite próprio, bem maior, porque todos os visitantes do site chegam pela mesma chave.
📦 E se eu não quiser escrever esses arquivos?
O exemplo deste artigo é pequeno de propósito, para dar para entender cada linha. Mas a API Mágica também entrega um kit pronto: o GET /v1/webpush/projetos/{id}/kit devolve, já preenchidos com a configuração do seu projeto, um webpush.js (413 linhas), um webpush-sw.js, um manifest.json e uma webpush-ponte.php (152 linhas).
O kit vem com mais coisa: um convite em forma de banner configurável, tags, cancelamento, e uma ponte que só aceita chamadas da própria origem. A instalação é colar duas tags na página:
<link rel="manifest" href="/manifest.json">
<script src="/webpush.js" defer></script>
O caminho é o mesmo do exemplo: service worker, inscrição, ponte, API. Então, entendendo os quatro arquivos pequenos, você entende o que o kit faz por baixo. 😉
🦄 O código completo
O site de exemplo inteiro está no GitHub, com um README que resume o passo a passo:
Para rodar: gere a chave no painel, copie o config.exemplo.php para config.php, rode php criar-projeto.php, cole as duas linhas que ele imprimir, e suba com php -S localhost:8000. Só precisa de PHP 7.4 ou mais novo, com a extensão curl.
E se você ainda não conhece a API Mágica, o artigo de apresentação mostra os outros recursos: CEP, Pix, QR Code e mais. Tudo gratuito. 🪄
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Perguntas frequentes
Preciso de alguma biblioteca de Web Push no PHP?
Por que o navegador não chama a API Mágica direto?
X-API-Key, e tudo que vai para o navegador qualquer visitante lê. Por isso existe a ponte.php: o navegador fala com o seu servidor, e só o servidor conhece a chave.Por que o Chrome diz "permission denied" se eu permiti as notificações?
userVisibleOnly. Com false, o Chrome recusa a inscrição com NotAllowedError: Registration failed - permission denied, mesmo com a permissão concedida. Ele só aceita userVisibleOnly: true.O que significa o erro "no active Service Worker"?
register() só registra; quem espera ele ativar é o navigator.serviceWorker.ready. Inscreva usando o registro que o ready devolve.Se o envio mostra "aceitos: 1", a notificação apareceu?
aceitos conta o que o serviço de push do navegador aceitou; ele não confirma que a notificação foi exibida. O sinal de que a pessoa viu é o contador cliques, que o service worker alimenta quando alguém clica.Funciona sem HTTPS?
localhost, que o navegador trata como seguro. Service worker exige HTTPS em qualquer outro endereço, então o site de verdade precisa de certificado.Leia também
API Mágica: CEP e Pix de graça, sem cartão
Lancei a API Mágica: CEP, QR Code Pix, geradores e mais, de graça. Veja como consultar e gerar com Node.js em poucas linhas.
Resend: enviando e recebendo e-mails com Node.js
Tutorial da Resend com Node.js: criar a chave, enviar com fetch, verificar o domínio e receber e-mails por webhook conferindo a assinatura.
Gerando a DANFE do XML da nota com Node.js
Como ler o XML de uma NF-e em Node.js, montar o HTML do DANFE e virar PDF com o Puppeteer, com o codigo de barras da chave.