Verificar a assinatura
Como o Vipter assina cada webhook, código testado em Node.js, PHP e Python para conferir a assinatura, como trocar o segredo sem perder eventos e os erros que mais fazem a verificação falhar.
Qualquer pessoa que descubra a URL do seu endpoint consegue mandar um POST para ela. A assinatura prova que o evento veio do Vipter e que o corpo não foi alterado no caminho. Verifique a assinatura em toda requisição, antes de ler o JSON.
Antes de começar
- O segredo do endpoint, que começa com
whsec_. Ele aparece uma vez só, logo depois de criar o endpoint ou de trocar o segredo. Veja Receber eventos no seu sistema. - Acesso ao corpo cru da requisição no seu framework, antes de qualquer conversão para JSON.
Como a assinatura é calculada
A cada tentativa de entrega, o Vipter:
- Pega a hora atual em segundos Unix:
t. - Monta o texto
<t>.<corpo>: o valor det, um ponto e o corpo exatamente como é enviado. - Calcula o HMAC SHA-256 desse texto, usando o segredo do endpoint como chave, e escreve o resultado em hexadecimal minúsculo: 64 caracteres.
- Envia o cabeçalho
Vipter-Signature: t=<t>,v1=<hmac>.
A chave do HMAC é o segredo inteiro, como texto, incluindo o prefixo whsec_. Não remova o prefixo nem decodifique o segredo.
Durante a troca de segredo, o cabeçalho traz um v1= para cada segredo válido, o atual primeiro:
Vipter-Signature: t=1790604192,v1=<hmac com o segredo novo>,v1=<hmac com o segredo anterior>Para conferir, recalcule o HMAC com o seu segredo e compare com cada v1=. Basta um igual. Depois, recuse t muito longe do relógio do seu servidor, para que uma requisição antiga capturada não possa ser repetida. Os exemplos abaixo aceitam até 5 minutos, a mesma tolerância do trecho que o painel mostra em Trecho de verificação (Node.js). Como t é recalculado a cada tentativa, as novas tentativas não esbarram nessa tolerância.
Código de verificação
Os três exemplos foram testados com assinaturas geradas do mesmo jeito que o Vipter gera: corpo com acentos, um e dois v1=, segredo anterior durante a troca, corpo alterado, corpo reformatado e t com 10 minutos de atraso. Cada um é um servidor completo, que responde 200 para uma assinatura válida e 400 para as outras.
A função é a mesma do painel. Ela recebe o corpo como texto. O servidor usa só a biblioteca padrão do Node.js 18 ou mais novo.
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyVipterSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return header.split(',').filter((p) => p.startsWith('v1=')).some((p) => {
const given = Buffer.from(p.slice(3), 'hex');
return given.length === expected.length / 2 && timingSafeEqual(given, Buffer.from(expected, 'hex'));
});
}
createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const rawBody = Buffer.concat(chunks).toString('utf8');
if (!verifyVipterSignature(rawBody, req.headers['vipter-signature'] ?? '', process.env.VIPTER_WEBHOOK_SECRET)) {
res.writeHead(400).end('assinatura inválida');
return;
}
const event = JSON.parse(rawBody);
// Registre event.id e ignore o evento se ele já foi processado.
res.writeHead(200).end(`ok ${event.type}`);
});
}).listen(Number(process.env.PORT ?? 8000));Com Express, use express.raw({ type: 'application/json' }) na rota do webhook e passe req.body.toString('utf8') para a função, como no exemplo de Receber eventos no seu sistema. Num route handler do Next.js, leia o corpo com await request.text().
Testar sem esperar um evento
Para testar o seu código localmente, gere uma assinatura com openssl, do mesmo jeito que o Vipter faz, e envie com curl:
SECRET="$VIPTER_WEBHOOK_SECRET"
URL="http://localhost:8000/"
BODY='{"id":"evt_000000000000000000000000","object":"event","type":"customer.created","created":1790604190,"livemode":false,"api_version":"2026-09-01","data":{"object":{"object":"customer","id":"cust_test","email":"test@example.com","name":"Test Customer","test":true}}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -sS -X POST "$URL" \
-H 'Content-Type: application/json' \
-H "Vipter-Signature: t=$T,v1=$SIG" \
-H 'Vipter-Event-Id: evt_000000000000000000000000' \
-H 'Vipter-Event-Type: customer.created' \
--data-binary "$BODY" -w ' HTTP %{http_code}\n'Os três servidores acima respondem ok customer.created HTTP 200. Com o endpoint publicado, use Enviar evento de teste no painel para o teste de ponta a ponta.
Trocar o segredo
O botão Rotacionar segredo gera um segredo novo e mostra uma vez só. O segredo anterior continua valendo por 24 horas a partir da troca.
Durante essas 24 horas, cada entrega traz dois v1=: um com o segredo novo e outro com o anterior. Como o código acima aceita qualquer v1= que confira, o seu servidor continua aceitando os eventos com o segredo antigo e passa a aceitar com o novo assim que você trocar a variável de ambiente. Para trocar sem perder eventos:
- Clique em Rotacionar segredo e copie o segredo novo.
- Atualize o segredo no seu servidor e publique, dentro de 24 horas.
- Mande um evento de teste e confira o status
succeededem Entregas recentes.
Depois das 24 horas, só o segredo novo assina. Trocar de novo dentro desse prazo invalida na hora o segredo mais antigo: só o atual e o imediatamente anterior valem ao mesmo tempo. Uma entrega reenviada é assinada no momento do envio, com os segredos válidos naquele momento.
Problemas comuns
-
A assinatura nunca confere
O corpo foi convertido para JSON e de volta para texto antes da verificação. Qualquer diferença, como espaços, ordem das chaves ou
éno lugar deé, muda o HMAC. Verifique o corpo cru. Em Express, umexpress.json()global consome o corpo antes da sua rota: registre a rota do webhook antes dele ou useexpress.rawsó nela. -
Confere em teste, falha com acentos
O corpo foi lido ou convertido com outra codificação. O HMAC é calculado sobre os bytes UTF-8 do corpo. Em Node.js, junte os
Buffere converta comutf8no fim, não pedaço a pedaço. -
Falha depois de alguns minutos, ou em todos os eventos de um servidor
O relógio do servidor está atrasado ou adiantado, e
tsai da tolerância de 5 minutos. Sincronize o relógio por NTP. Aumentar a tolerância esconde o problema e enfraquece a proteção contra repetição. -
Falha com o segredo certo
O segredo é de outro endpoint, o prefixo
whsec_foi removido, ou há um espaço ou quebra de linha no fim da variável de ambiente. Cada endpoint tem o seu segredo. O card do endpoint mostra o final do segredo atual para você conferir. -
Verifica o
createdem vez dotA assinatura usa o
tdo cabeçalho. Ocreateddo corpo é a hora do evento e não muda nas novas tentativas. -
Compara com
==Uma comparação comum de texto pode vazar, pelo tempo de resposta, quantos caracteres conferem. Use
timingSafeEqualno Node.js,hash_equalsno PHP ehmac.compare_digestno Python.
O que fazer a seguir
- Veja como tratar eventos repetidos e fora de ordem.
- Entenda o calendário de tentativas quando a verificação ou o servidor falham.
Formato do envelope
A requisição que o Vipter faz ao seu endpoint, campo a campo, com os cabeçalhos, o prazo de resposta, como tratar eventos repetidos e o que o Vipter garante sobre a ordem.
Tentativas, desativação e reenvio
Quando o Vipter tenta de novo uma entrega que falhou, quando desativa o endpoint sozinho, o que acontece com os eventos enquanto ele está desligado e como usar o evento de teste e o reenvio do painel.