A maioria dos bugs de webhook do Shopify no desenvolvimento local se resume a uma linha: o HMAC é codificado em base64, não em hex. Se você chama .digest('hex') e compara com X-Shopify-Hmac-Sha256, a verificação nunca vai passar — mesmo com o segredo compartilhado correto. Já vimos engenheiros sênior perderem uma tarde por causa desse um caractere.
A armadilha do base64
O Stripe usa hex. O GitHub usa hex (com prefixo sha256=). O Shopify usa base64. Três provedores, três codificações. Sua primeira versão do verificador quase certamente vai copiar um snippet de outro projeto e falhar em silêncio.
Eis como a verificação deveria parecer:
const hmac = crypto
.createHmac('sha256', SHOPIFY_API_SECRET)
.update(rawBody) // raw body, not JSON-parsed
.digest('base64'); // base64, not hex
const valid = crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(req.headers['x-shopify-hmac-sha256']),
);
Duas coisas importam além da codificação. O corpo tem que ser os bytes crus, sem parse. E a comparação tem que ser de tempo constante — a igualdade de strings vaza informação byte a byte.
Por que um túnel facilita isso
Você também pode usar o comando do Shopify shopify app dev, que sobe um túnel interno. Funciona. Mas também embrulha seu servidor de desenvolvimento no próprio processo, engole logs de um jeito específico e não te dá botão de replay. Para desenvolvimento de app além do "hello world", uma URL pública estável mais um túnel localhost com captura de requisições economiza mais tempo do que a CLI economiza na configuração inicial.
npx portpreview 3000
Você cola a URL HTTPS na configuração de webhooks do seu app no painel de Partners, dispara um evento de uma loja de teste, e a requisição aterrissa no seu handler local com todos os cabeçalhos intactos.
Lojas de teste: a parte que a documentação do Shopify passa por cima
Duas coisas para saber antes de ligar qualquer coisa:
- Lojas de desenvolvimento disparam todos os eventos de webhook. Criação de pedido, fulfillment, estoque — tudo. Você não precisa falsear nada; só instale seu app numa loja dev e clique por aí.
- O segredo do webhook difere por app e por canal de entrega. Se você também assina o EventBridge ou Pub/Sub, o comportamento do HMAC é diferente. Aqui falamos de entrega de webhook HTTPS pura.
Configuração passo a passo
- Inicie seu app localmente na porta que você usa (3000 é comum).
- Rode
npx portpreview 3000e copie a URL HTTPS que ele imprime. - No painel de Partners do Shopify, abra seu app e vá em Configuration → Webhooks.
- Defina o endpoint do webhook como
https://your-tunnel.portpreview.dev/api/webhooks/shopify(ou o path que seu app usa). - Instale o app numa loja dev, depois dispare um evento — crie um pedido rascunho, faça fulfillment de um item, mude o estoque.
- Veja a requisição aterrissar. Inspecione cabeçalhos e corpo. Faça replay da requisição capturada após cada correção do handler.
Os erros que continuamos vendo
Corpo parseado antes da verificação
Se você colocar app.use(express.json()) antes da sua rota de webhook, os bytes crus já se foram quando você tenta verificar. Monte um parser de corpo cru só no path do webhook, ou puxe o corpo manualmente do stream da requisição antes de qualquer parsing JSON.
Segredos trocados
O segredo do app de Partners não é o mesmo que o token da Storefront API. O HMAC do webhook usa o segredo do app. Se você está olhando para shp_xxx no seu arquivo env, pegou o errado.
Eventos de teste que parecem idênticos
O botão "Send test notification" do Shopify entrega um evento sintético com corpo de placeholder. A assinatura desse evento de teste é real, mas o payload é fixo. Para testes realistas de formato de payload, dispare eventos da própria loja dev.
Replay não é negociável para dev de app
O Shopify tenta novamente webhooks falhos por até 48 horas com backoff exponencial. É generoso, mas quando você itera, não quer esperar a próxima tentativa. Capture a primeira entrega no histórico de requisições do seu túnel e faça replay sob demanda contra seu handler local.
Quando o local deixa de bastar
Fluxos de registro de loja, webhooks de GDPR e mudanças de cobrança de assinatura são mais fáceis de validar contra um ambiente implantado porque interagem com o próprio estado de conta do Shopify. Para todo o resto — parsing de payload, verificação de assinatura, lógica de negócio — local é mais rápido.
Para a matemática de assinatura subjacente em todos os provedores, leia nosso guia de verificação de assinatura de webhooks. Entre na lista de espera do PortPreview se quiser um túnel com replay embutido.