Skip to main content
Os webhooks da API Vexy Bank permitem que sua aplicação receba notificações automáticas quando eventos importantes acontecem em sua conta, como pagamentos recebidos, transferências concluídas e mudanças de status.

Como funcionam

Os webhooks são enviados automaticamente via POST para as URLs que você configurar sempre que eventos relevantes ocorrem em sua conta. Isso elimina a necessidade de fazer polling constante na API.

Vantagens

Tempo real

Receba notificações instantâneas sobre mudanças importantes

Confiabilidade

Sistema de retry automático para garantir entrega

Segurança

Verificação de assinatura HMAC-SHA256 para autenticidade

Flexibilidade

Configure diferentes endpoints para diferentes tipos de evento

Eventos disponíveis

Eventos de transação (PIX IN)

EventoDescriçãoQuando é enviado
transaction_createdTransação criadaQR Code gerado com sucesso
transaction_paidTransação pagaPIX recebido e confirmado
transaction_refundedTransação estornadaEstorno processado
transaction_infractionInfração na transaçãoProblemas detectados pelo BC

Eventos de transferência (PIX OUT)

EventoDescriçãoQuando é enviado
transfer_createdTransferência criadaTransferência iniciada
transfer_completedTransferência concluídaPIX enviado com sucesso
transfer_canceledTransferência canceladaTransferência cancelada
transfer_updatedTransferência atualizadaStatus alterado

Estrutura de payloads

Webhook de Transação (PIX IN)

{
  "id": "wh_64f8a2b1c3d4e5f6g7h8i9j0",
  "type": "transaction",
  "event": "transaction_paid",
  "scope": "user",
  "transaction": {
    "id": "trx_1a2b3c4d5e6f7g8h9i0j",
    "amount": 5000,
    "status": "paid",
    "pix": {
      "endToEndId": "E00000000202401011200000000000000",
      "payerInfo": {
        "name": "Cliente Pagador",
        "document": "11122233344"
      }
    }
  }
}

Webhook de Transferência (PIX OUT)

{
  "id": "transfer_abc123def456",
  "type": "transfer",
  "event": "transfer_completed",
  "scope": "postback",
  "transfer": {
    "id": "transfer_abc123def456",
    "amount": 10000,
    "status": "completed",
    "pix": {
      "endToEndId": "E00000000202401011200000000000000",
      "creditorAccount": null
    }
  }
}

Verificação de assinatura

IMPORTANTE: Sempre verifique a assinatura dos webhooks para garantir autenticidade e segurança.
Toda notificação que enviamos para o seu endpoint é assinada. Fazemos isso incluindo um header com o nome Vexy-Signature em cada evento que enviamos. Isso permite verificar e garantir que o evento foi enviado pelo Vexy Bank e não por um terceiro.

Formato do Header Vexy-Signature

O header Vexy-Signature contém um timestamp e uma ou mais assinaturas. O timestamp é prefixado por t= e cada assinatura é prefixada por um schema. Schemas começam com v seguido de um integer. Atualmente existe apenas um schema de assinatura que é o v1. Exemplo do Header Vexy-Signature:
Vexy-Signature: t=1580306324381,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
As assinaturas são geradas usando HMAC com SHA-256. Para prevenir ataques de reversão de versão (downgrade attacks), você deve ignorar todos os schemas que não são v1.

Como Validar a Assinatura

Passo 1: Extrair o timestamp e assinatura do Header

Faça um split no header usando o caractere , como separador para pegar a lista de elementos. Feito isso, faça outro split usando o caractere = como separador, para pegar o prefixo e o valor. O valor obtido no prefixo t corresponde ao timestamp e o v1 corresponde à assinatura. Você pode descartar outros valores.

Passo 2: Preparar a string para comparar as assinaturas

Você deve concatenar essas informações:
  1. O timestamp (como string)
  2. O caractere .
  3. O payload JSON raw (corpo da requisição, em formato de string)
Computar o HMAC com a função hash SHA256. Use o signatureSecret recebido na hora da criação do webhook e use a string signed_payload como mensagem.

Passo 3: Comparar as assinaturas

Compare a assinatura enviada pelo Vexy Bank no header com a assinatura que você gerou no Passo 2.

Exemplo Completo em Node.js

const crypto = require('crypto')

function verifyWebhookSignature(requestHeaders, requestPayload, secretKey) {
  // Passo 1: Extrair timestamp e assinatura do header
  const signatureHeader = requestHeaders['vexy-signature']
  
  if (!signatureHeader) {
    throw new Error('Header Vexy-Signature não encontrado')
  }
  
  const elements = signatureHeader.split(',')
  let timestamp, signature
  
  for (const element of elements) {
    const [prefix, value] = element.split('=')
    if (prefix === 't') {
      timestamp = value
    } else if (prefix === 'v1') {
      signature = value
    }
  }
  
  if (!timestamp || !signature) {
    throw new Error('Timestamp ou assinatura não encontrados no header')
  }
  
  // Passo 2: Preparar a string para comparação
  const signedPayload = `${timestamp}.${requestPayload}`
  
  // Computar HMAC
  const computedSignature = crypto
    .createHmac('sha256', secretKey)
    .update(signedPayload)
    .digest('hex')
  
  // Passo 3: Comparar assinaturas
  return signature === computedSignature
}

// Uso no seu endpoint webhook
app.post('/webhooks/vexy-bank', express.raw({type: 'application/json'}), (req, res) => {
  try {
    const secretKey = 'whk_live_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4' // Secret recebido na criação do webhook
    const requestPayload = req.body.toString() // Payload raw como string
    
    if (!verifyWebhookSignature(req.headers, requestPayload, secretKey)) {
      return res.status(401).send('Signature verification failed')
    }
    
    // Processar webhook seguramente
    const event = JSON.parse(requestPayload)
    console.log('Webhook verificado:', event)
    res.status(200).send('OK')
    
  } catch (error) {
    console.error('Erro na verificação:', error.message)
    res.status(401).send('Unauthorized')
  }
})

// Exemplo detalhado com valores reais
const secretKey = 'whk_live_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4'
const timestamp = '1580306991086'
const requestPayload = '{"event":"transaction_paid","transaction":{"id":"abc123","amount":10000}}'

// String para assinar: timestamp + '.' + payload
const signedPayload = `${timestamp}.${requestPayload}`

// Gerar assinatura
const signature = crypto
  .createHmac('sha256', secretKey)
  .update(signedPayload)
  .digest('hex')

console.log('Assinatura gerada:', signature)
// Output: 348a92ec7864e30fc9cf3ea91b2e6e1392a14c8379103cb1d8e48e39334a4fd8

Exemplo Completo em Python

import hmac
import hashlib

def verify_webhook_signature(request_headers, request_payload, secret_key):
    """
    Verifica a assinatura do webhook Vexy Bank
    """
    # Passo 1: Extrair timestamp e assinatura do header
    signature_header = request_headers.get('vexy-signature')
    
    if not signature_header:
        raise ValueError('Header Vexy-Signature não encontrado')
    
    elements = signature_header.split(',')
    timestamp = None
    signature = None
    
    for element in elements:
        prefix, value = element.split('=')
        if prefix == 't':
            timestamp = value
        elif prefix == 'v1':
            signature = value
    
    if not timestamp or not signature:
        raise ValueError('Timestamp ou assinatura não encontrados no header')
    
    # Passo 2: Preparar a string para comparação
    signed_payload = f"{timestamp}.{request_payload}"
    
    # Computar HMAC
    computed_signature = hmac.new(
        secret_key.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Passo 3: Comparar assinaturas
    return signature == computed_signature

# Uso no Flask
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhooks/vexy-bank', methods=['POST'])
def handle_webhook():
    try:
        secret_key = 'whk_live_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4'
        request_payload = request.get_data().decode('utf-8')  # Payload raw como string
        
        if not verify_webhook_signature(request.headers, request_payload, secret_key):
            return 'Unauthorized', 401
        
        # Processar webhook seguramente
        import json
        event = json.loads(request_payload)
        print('Webhook verificado:', event)
        return 'OK', 200
        
    except Exception as error:
        print(f'Erro na verificação: {error}')
        return 'Unauthorized', 401

# Exemplo detalhado com valores reais
secret_key = 'whk_live_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4'
timestamp = '1580306991086'
request_payload = '{"event":"transaction_paid","transaction":{"id":"abc123","amount":10000}}'

# String para assinar: timestamp + '.' + payload
signed_payload = f"{timestamp}.{request_payload}"

# Gerar assinatura
signature = hmac.new(
    secret_key.encode('utf-8'),
    signed_payload.encode('utf-8'),
    hashlib.sha256
).hexdigest()

print(f'Assinatura gerada: {signature}')
# Output: 348a92ec7864e30fc9cf3ea91b2e6e1392a14c8379103cb1d8e48e39334a4fd8

Exemplo Completo em PHP

<?php

function verifyWebhookSignature($requestHeaders, $requestPayload, $secretKey) {
    // Passo 1: Extrair timestamp e assinatura do header
    $signatureHeader = $requestHeaders['vexy-signature'] ?? $requestHeaders['Vexy-Signature'] ?? null;
    
    if (!$signatureHeader) {
        throw new Exception('Header Vexy-Signature não encontrado');
    }
    
    $elements = explode(',', $signatureHeader);
    $timestamp = null;
    $signature = null;
    
    foreach ($elements as $element) {
        list($prefix, $value) = explode('=', $element, 2);
        if ($prefix === 't') {
            $timestamp = $value;
        } elseif ($prefix === 'v1') {
            $signature = $value;
        }
    }
    
    if (!$timestamp || !$signature) {
        throw new Exception('Timestamp ou assinatura não encontrados no header');
    }
    
    // Passo 2: Preparar a string para comparação
    $signedPayload = $timestamp . '.' . $requestPayload;
    
    // Computar HMAC
    $computedSignature = hash_hmac('sha256', $signedPayload, $secretKey);
    
    // Passo 3: Comparar assinaturas
    return hash_equals($signature, $computedSignature);
}

// Uso
try {
    $secretKey = 'whk_live_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4';
    $requestPayload = file_get_contents('php://input'); // Payload raw como string
    $requestHeaders = getallheaders();
    
    if (!verifyWebhookSignature($requestHeaders, $requestPayload, $secretKey)) {
        http_response_code(401);
        exit('Unauthorized');
    }
    
    // Processar webhook seguramente
    $event = json_decode($requestPayload, true);
    error_log('Webhook verificado: ' . print_r($event, true));
    echo 'OK';
    
} catch (Exception $error) {
    error_log('Erro na verificação: ' . $error->getMessage());
    http_response_code(401);
    exit('Unauthorized');
}

// Exemplo detalhado com valores reais
$secretKey = 'whk_live_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4';
$timestamp = '1580306991086';
$requestPayload = '{"event":"transaction_paid","transaction":{"id":"abc123","amount":10000}}';

// String para assinar: timestamp + '.' + payload
$signedPayload = $timestamp . '.' . $requestPayload;

// Gerar assinatura
$signature = hash_hmac('sha256', $signedPayload, $secretKey);

echo "Assinatura gerada: " . $signature . "\n";
// Output: 348a92ec7864e30fc9cf3ea91b2e6e1392a14c8379103cb1d8e48e39334a4fd8
?>

⚠️ Importante sobre o Payload

CRÍTICO: O payload JSON deve ser mantido exatamente como foi recebido, sem qualquer conversão ou formatação. Qualquer alteração, como transformá-lo em um objeto e depois reconvertê-lo em string (por exemplo, usando JSON.stringify), pode resultar em diferenças que invalidariam a assinatura calculada.

Exemplo de Extração de Valores

// Exemplo do Header da requisição enviada pelo Vexy Bank:
const requestHeaders = {
  'Vexy-Signature': 't=1580306991086,v1=348a92ec7864e30fc9cf3ea91b2e6e1392a14c8379103cb1d8e48e39334a4fd8'
}

// Extrair o valor de 't' do Header 'Vexy-Signature'
const reqTimestamp = requestHeaders['Vexy-Signature'].split(',')[0].split('=')[1]
// '1580306991086'

// Extrair o valor de 'v1' do Header 'Vexy-Signature'
const reqSignature = requestHeaders['Vexy-Signature'].split(',')[1].split('=')[1]
// '348a92ec7864e30fc9cf3ea91b2e6e1392a14c8379103cb1d8e48e39334a4fd8'

// A assinatura que você gerou no Passo 2 deve ser igual ao valor da variável "reqSignature"
console.log('yourSignature' === reqSignature) // Deve retornar true

Validação de Timestamp (Opcional)

function validateTimestamp(timestamp, toleranceSeconds = 300) {
  const now = Math.floor(Date.now() / 1000)
  const webhookTime = Math.floor(parseInt(timestamp) / 1000)
  
  return Math.abs(now - webhookTime) <= toleranceSeconds
}

// Uso
if (!validateTimestamp(timestamp)) {
  throw new Error('Timestamp muito antigo ou muito novo')
}

Certificados mTLS para webhooks

IMPORTANTE: Por norma de segurança, é necessário configurar certificados mTLS (autenticação mútua) em seu servidor para receber webhooks do Vexy Bank.

Entendendo o padrão mTLS

Para garantir a segurança na comunicação, será necessário inserir a chave pública do Vexy Bank em seu servidor para que a comunicação obedeça o padrão mTLS. No domínio que representa seu servidor, você deverá configurar a exigência da chave pública (mTLS) que estamos disponibilizando, para que ocorra a autenticação mútua.

Como funciona a validação

O Vexy Bank fará 2 requisições para seu domínio (servidor):
  1. Primeira Requisição: Vamos certificar que seu servidor esteja exigindo uma chave pública do Vexy Bank. Para isso, enviaremos uma requisição sem certificado e seu servidor não deverá aceitar a requisição. Caso seu servidor responda com recusa, enviaremos a 2ª requisição.
  2. Segunda Requisição: Seu servidor, que deve conter a chave pública disponibilizada, deverá realizar o “Hand-Shake” para que a comunicação seja estabelecida.

Requisitos Técnicos

  • Versão mínima do TLS: 1.2
  • Resposta padrão: Configure uma rota ‘POST’ com uma resposta padrão como string “200”
  • Certificado: Inclua nosso certificado de produção ou homologação em seu servidor

Certificados Disponíveis

Certificado de produção

URL: http://api.vexybank.com/caPara uso em ambiente de produção

Certificado de homologação

URL: http://api.vexybank.com/caPara uso em ambiente de desenvolvimento/teste

Exemplo de Configuração do Servidor

Nginx

server {
    listen 443 ssl;
    server_name seu-dominio.com;
    
    # Certificados SSL normais
    ssl_certificate /path/to/your/cert.pem;
    ssl_certificate_key /path/to/your/key.pem;
    
    # Configuração mTLS para webhook
    location /webhooks/vexy-bank {
        # Exigir certificado do cliente
        ssl_verify_client on;
        
        # Certificado CA do Vexy Bank
        ssl_client_certificate /path/to/vexy-bank-ca.crt;
        
        # Protocolo TLS mínimo
        ssl_protocols TLSv1.2 TLSv1.3;
        
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Apache

<VirtualHost *:443>
    ServerName seu-dominio.com
    
    SSLEngine on
    SSLCertificateFile /path/to/your/cert.pem
    SSLCertificateKeyFile /path/to/your/key.pem
    
    # Configuração mTLS para webhook
    <Location "/webhooks/vexy-bank">
        SSLVerifyClient require
        SSLCACertificateFile /path/to/vexy-bank-ca.crt
        SSLProtocol TLSv1.2 +TLSv1.3
        
        ProxyPass http://localhost:3000/webhooks/vexy-bank
        ProxyPassReverse http://localhost:3000/webhooks/vexy-bank
    </Location>
</VirtualHost>

Instalação do Certificado

1. Baixar o Certificado

# Baixar certificado do Vexy Bank
curl -o vexy-bank-ca.crt http://api.vexybank.com/ca

# Verificar o certificado
openssl x509 -in vexy-bank-ca.crt -text -noout

2. Instalar no Servidor

# Mover para diretório de certificados
sudo mkdir -p /etc/ssl/certs/vexy-bank
sudo mv vexy-bank-ca.crt /etc/ssl/certs/vexy-bank/

# Definir permissões corretas
sudo chmod 644 /etc/ssl/certs/vexy-bank/vexy-bank-ca.crt

Servidores Dedicados

Recomendação: Use um servidor dedicado para configurar webhooks, pois é necessário acesso a arquivos de configuração do servidor para implementar mTLS corretamente.

Skip-mTLS (Hospedagem Compartilhada)

Para hospedagem em servidores compartilhados, onde pode haver restrições para inserir certificados de outras entidades, disponibilizamos a opção skip-mTLS.

Como funciona

  • Permite cadastro de webhook sem validação mTLS
  • Você fica responsável por validar nosso certificado
  • Sempre enviamos o certificado nos webhooks
  • Implementação de medidas de segurança adicionais é obrigatória

Medidas de Segurança Recomendadas

Restrinja a comunicação para aceitar apenas do IP da Vexy Bank:
location /webhooks/vexy-bank {
    allow 34.193.116.226;  # IP da Vexy Bank
    deny all;
    
    # Restante da configuração...
}
Adicione um HMAC à URL para validação:
// Exemplo de implementação
const crypto = require('crypto')

function generateWebhookHash(secret) {
  return crypto
    .createHmac('sha256', secret)
    .update('webhook-validation')
    .digest('hex')
}

// URL com hash
const webhookUrl = `https://seu-dominio.com/webhook?hmac=${generateWebhookHash('seu-secret')}&ignorar=`
Exemplo de URL:
Original: https://seu-dominio.com/webhook
Com hash: https://seu-dominio.com/webhook?hmac=xyz123&ignorar=
O termo ignorar= serve para tratar a adição automática de /pix no final da URL pelo sistema.

Validação de Certificados (Skip-mTLS)

const crypto = require('crypto')
const https = require('https')

// Validar certificado do Vexy Bank
function validateVexyCertificate(cert) {
  // Verificar se o certificado é do Vexy Bank
  const expectedIssuer = 'Vexy Bank CA'
  
  if (!cert.issuer.CN.includes(expectedIssuer)) {
    throw new Error('Certificado não é do Vexy Bank')
  }
  
  // Verificar se não expirou
  const now = new Date()
  const notAfter = new Date(cert.valid_to)
  
  if (now > notAfter) {
    throw new Error('Certificado expirado')
  }
  
  return true
}

// Middleware para validar certificado
app.use('/webhooks/vexy-bank', (req, res, next) => {
  const clientCert = req.connection.getPeerCertificate()
  
  if (!clientCert || !validateVexyCertificate(clientCert)) {
    return res.status(401).send('Certificado inválido')
  }
  
  next()
})

Teste de Configuração

# Testar configuração mTLS
curl -v \
  --cert /path/to/vexy-bank-ca.crt \
  --key /path/to/vexy-bank-ca.key \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"test": true}' \
  https://seu-dominio.com/webhooks/vexy-bank

Implementação recomendada

1. Endpoint Webhook Robusto

app.post('/webhooks/vexy-bank', express.raw({type: 'application/json'}), (req, res) => {
  try {
    // 1. Verificar assinatura
    const signature = req.headers['vexy-signature']
    if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
      return res.status(401).send('Unauthorized')
    }

    // 2. Parse do payload
    const event = JSON.parse(req.body)

    // 3. Processamento idempotente
    if (await isEventProcessed(event.id)) {
      return res.status(200).send('Already processed')
    }

    // 4. Processar evento
    await processWebhookEvent(event)

    // 5. Marcar como processado
    await markEventAsProcessed(event.id)

    res.status(200).send('OK')
  } catch (error) {
    console.error('Webhook error:', error)
    res.status(500).send('Internal server error')
  }
})

2. Processamento por Tipo de Evento

async function processWebhookEvent(event) {
  switch (event.event) {
    case 'transaction_paid':
      await handleTransactionPaid(event.transaction)
      break
    
    case 'transfer_completed':
      await handleTransferCompleted(event.transfer)
      break
    
    default:
      console.log('Evento não tratado:', event.event)
  }
}

async function handleTransactionPaid(transaction) {
  // Liberar produto/serviço
  // Enviar email de confirmação
  // Atualizar banco de dados
  console.log(`Pagamento recebido: R$ ${transaction.amount / 100}`)
}

Sistema de retry

Se seu endpoint não responder com status 200, implementamos um sistema de retry automático:
  • 1ª tentativa: Imediatamente
  • 2ª tentativa: Após 1 minuto
  • 3ª tentativa: Após 5 minutos
  • 4ª tentativa: Após 15 minutos
  • 5ª tentativa: Após 1 hora
Após 5 tentativas sem sucesso, o webhook é marcado como falhado e você pode visualizar no dashboard.

Monitoramento

Dashboard de Webhooks

No seu dashboard você pode:
  • Ver histórico de webhooks enviados
  • Verificar status de entrega
  • Reenviar webhooks falhados
  • Visualizar logs detalhados

Logs Úteis

// Log estruturado para debugging
console.log({
  timestamp: new Date().toISOString(),
  webhookId: event.id,
  eventType: event.event,
  processed: true,
  processingTime: Date.now() - startTime
})

Solução de problemas

Problemas Comuns

  • Verifique se a URL está acessível publicamente
  • Confirme se está respondendo com status 200
  • Teste com ferramentas como ngrok para desenvolvimento local
  • Verifique se não há firewall bloqueando
  • Confirme se está usando o signatureSecret correto
  • Verifique se o payload não foi modificado
  • Use o body raw da requisição para verificação
  • Certifique-se de usar UTF-8 encoding
  • Implemente processamento idempotente
  • Use o id do evento para deduplicação
  • Armazene IDs processados em cache/banco
  • Sempre responda 200 para eventos já processados

Suporte

Para configurar webhooks ou resolver problemas: Email: [email protected]
WhatsApp: +55 11 99999-9999
Documentação: Gerenciar Webhooks
Dica: Use ferramentas como webhook.site para testar e debuggar seus webhooks durante o desenvolvimento.