Skip to Content
docs
Webhooks

Implementação

Exemplos Completos

Os exemplos abaixo recebem os eventos de transação, validam a assinatura X-Avista-Signature, respondem 2xx rapidamente e evitam processamento duplicado. A geração da chave e os detalhes da assinatura estão em Assinatura HMAC.


Assinatura HMAC (X-Avista-Signature)

Depois que você gera a chave de assinatura no painel, todo webhook enviado para a sua conta inclui o header X-Avista-Signature com o HMAC-SHA256, em hexadecimal, do corpo bruto da requisição, calculado com essa chave. Validar a assinatura garante que o webhook veio da Gateways (e não de um terceiro que descobriu sua URL) e que o payload não foi alterado em trânsito.

O nome do header é mesmo X-Avista-Signature. Esse é o nome técnico enviado hoje; use-o exatamente assim no seu validador (em PHP, $_SERVER['HTTP_X_AVISTA_SIGNATURE']).

A chave é por conta: uma única chave assina todos os eventos (transações e MED), em todas as URLs cadastradas. O reenvio manual também é assinado com a chave atual da conta.

Enquanto nenhuma chave for gerada, os webhooks são entregues sem o header X-Avista-Signature. Se você está integrando do zero, gere a chave antes de colocar seu validador em produção e rejeite requisições sem o header, não apenas as que trazem assinatura incorreta.

Gerando e rotacionando a chave

A chave é gerada e rotacionada somente pelo painel (https://<dashboard-host>); não há endpoint de API para isso.

Acesse Configurações → Webhooks e localize o card Chave de assinatura HMAC.

Clique em Rotacionar e confirme sua senha. A mesma ação gera a primeira chave e substitui uma chave existente.

A chave aparece uma única vez, em uma janela que só fecha depois que você marca a confirmação de que a copiou. Depois disso, o painel mostra apenas o final da chave (últimos 4 caracteres) e a data da última rotação. A Gateways não consegue recuperar a chave: se perdê-la, gere uma nova.

Guarde a chave em um secret manager ou variável de ambiente (nunca em arquivo versionado):

export PIX_API_WEBHOOK_SECRET=<chave-gerada-no-painel>

A partir desse momento, os próximos webhooks já chegam com o header X-Avista-Signature.

Rotação sem rejeitar webhooks em trânsito

A rotação substitui a chave imediatamente: os webhooks enviados a partir daí usam a nova chave. Webhooks que já estavam na fila podem ainda chegar assinados com a chave anterior. Por isso, durante a troca, aceite as duas chaves por cerca de 30 minutos:

const SECRETS = [
  process.env.PIX_API_WEBHOOK_SECRET_CURRENT,
  process.env.PIX_API_WEBHOOK_SECRET_PREVIOUS,
].filter(Boolean) as string[];

function isValid(rawBody: Buffer, received: string): boolean {
  const b = Buffer.from(received, 'hex');
  return SECRETS.some((secret) => {
    const a = Buffer.from(
      crypto.createHmac('sha256', secret).update(rawBody).digest('hex'),
      'hex',
    );
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

Depois desse período, remova PIX_API_WEBHOOK_SECRET_PREVIOUS. Rotacione quando houver suspeita de vazamento ou em rotações programadas (por exemplo, a cada 90 dias).

Como validar

A assinatura cobre exatamente o corpo bruto da requisição (rawBody), em UTF-8, antes de qualquer JSON.parse. Se você reserializar o objeto recebido para calcular o HMAC, qualquer divergência (ordem de chaves, espaços, encoding) fará a assinatura não bater.

Use sempre comparação timing-safe (crypto.timingSafeEqual / hmac.compare_digest / hmac.Equal / hash_equals). Comparar com == abre brecha para ataques de timing.

Encoding: o corpo é UTF-8. Em linguagens que precisam de bytes explícitos (Java/Kotlin/PHP), leia o stream como bytes e não converta para String em outro encoding antes de calcular o HMAC. Em Python, request.get_data() já retorna bytes; em Node, Buffer é o tipo correto.

A assinatura HMAC não previne replay. Ela prova autenticidade e integridade, mas não impede que alguém que capture um webhook legítimo (proxy comprometido, log vazado) o reenvie. Combine a validação da assinatura com a idempotência e com a allowlist de IPs.

Headers customizados

Os headers customizados cadastrados na URL (até 5) são enviados em todo webhook, junto com a assinatura. Se você já valida um header próprio (por exemplo, Authorization: Basic ...), pode continuar validando: o X-Avista-Signature é adicional e não substitui esses headers.


Idempotência

Webhooks podem ser entregues mais de uma vez (retentativas e reenvio manual). Implemente idempotência para evitar processamento duplicado.

Para eventos de transação, use como chave a combinação event + transactionId + status. Assim, uma mesma transação que mude de status gera um novo processamento, e repetições do mesmo evento são ignoradas. Para eventos MED, use medId + event.

const key = `webhook:${payload.event}:${payload.transactionId}:${payload.status}`;

const isNew = await redis.set(key, '1', 'EX', 86400, 'NX');

if (!isNew) {
  console.log('Webhook já processado, ignorando');
  return;
}

await processWebhook(payload);


Boas Práticas


Retentativas

Se o seu endpoint não responder com 2xx em até 10 segundos (ou se a conexão falhar), a entrega é retentada:

TentativaIntervaloTempo acumulado
1ªImediato0 min
2ª (1º retry)30 segundos30 s
3ª (2º retry)1 minuto1 min 30 s
4ª (3º retry)3 minutos4 min 30 s
5ª (4º retry)5 minutos9 min 30 s
6ª (5º retry)15 minutos24 min 30 s

Após 6 tentativas sem sucesso (cerca de 25 minutos), a entrega automática é encerrada. Use o reenvio manual ou consulte a transação na API para não perder nenhum evento.

Com mais de uma URL cadastrada para o evento, as retentativas valem só para as URLs que falharam. URLs que já receberam com sucesso não recebem o mesmo evento de novo.


Códigos de Resposta

Seu endpoint deve retornar um código HTTP apropriado:

CódigoDescriçãoAção do Sistema
2xxSucesso (200, 201, 204, etc.)Webhook confirmado, não será retentado
3xxRedirecionamentoNão é seguido; considerado falha e retentado
4xxErro do clienteConsiderado falha e retentado
5xxErro do servidorConsiderado falha e retentado

O sistema avalia apenas o código HTTP. Qualquer resposta 2xx (200-299) é sucesso, independente do body. Você pode retornar body vazio, "OK" ou qualquer JSON.


Próximos Passos

On this page