VipterCentral de Ajuda
Desenvolvedores

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.

Admin ou DonoTodos os planos

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:

  1. Pega a hora atual em segundos Unix: t.
  2. Monta o texto <t>.<corpo>: o valor de t, um ponto e o corpo exatamente como é enviado.
  3. Calcula o HMAC SHA-256 desse texto, usando o segredo do endpoint como chave, e escreve o resultado em hexadecimal minúsculo: 64 caracteres.
  4. 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.

webhook.mjs
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:

  1. Clique em Rotacionar segredo e copie o segredo novo.
  2. Atualize o segredo no seu servidor e publique, dentro de 24 horas.
  3. Mande um evento de teste e confira o status succeeded em 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, um express.json() global consome o corpo antes da sua rota: registre a rota do webhook antes dele ou use express.raw só 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 Buffer e converta com utf8 no 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 t sai 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 created em vez do t

    A assinatura usa o t do cabeçalho. O created do 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 timingSafeEqual no Node.js, hash_equals no PHP e hmac.compare_digest no Python.

O que fazer a seguir

Nesta página