Visão Geral
Webhooks permitem que você receba notificações em tempo real sobre eventos importantes na sua conta Vision Wallet, como pagamentos confirmados, saques processados e mudanças de status.
Configurar Webhook
Configure o webhook na criação de uma API Key ou atualize o webhook da API Key principal:
curl -X PUT 'https://api.visionwallet.com.br/api/v1/user/api-key/main' \
-H 'X-API-Key: sua_api_key' \
-H 'Content-Type: application/json' \
-d '{
"webhookUrl": "https://seusite.com/webhook"
}'
Eventos Enviados
A Vision Wallet envia webhooks para os seguintes eventos:
Pagamento Aprovado (payment.approved)
Quando um pagamento PIX é confirmado e aprovado:
{
"event": "payment.approved",
"data": {
"txid": "payment_abc123",
"amount": "100.00",
"netAmount": "99.50",
"fee": "0.50",
"status": "approved",
"originalAmount": "100.00",
"feePassedToCustomer": true,
"description": "Pagamento de serviço",
"createdAt": 1705312200000,
"approvedAt": 1705312500000
},
"timestamp": 1705312500000
}
Pagamento Expirado (payment.expired)
Quando um pagamento expira sem ser pago:
{
"event": "payment.expired",
"data": {
"txid": "payment_abc123",
"amount": "100.00",
"status": "expired",
"description": "Pagamento de serviço",
"createdAt": 1705312200000,
"expiredAt": 1705315800000
},
"timestamp": 1705315800000
}
Pagamento Reembolsado (payment.refunded)
Quando um pagamento é reembolsado:
{
"event": "payment.refunded",
"data": {
"txid": "payment_abc123",
"amount": "100.00",
"status": "refunded",
"description": "Pagamento de serviço",
"createdAt": 1705312200000,
"refundedAt": 1705316000000
},
"timestamp": 1705316000000
}
Saque Processado (withdraw.completed ou withdrawal.completed)
Quando um saque é processado com sucesso:
{
"event": "withdraw.completed",
"data": {
"txid": "withdraw_abc123",
"amount": "100.00",
"fee": "0.50",
"sent": "99.50",
"pixKey": "usuario@example.com",
"pixKeyType": "EMAIL",
"status": "completed",
"createdAt": 1705312200000
},
"timestamp": 1705312205000
}
Saque Falhou (withdraw.failed ou withdrawal.failed)
Quando um saque falha:
{
"event": "withdraw.failed",
"data": {
"txid": "withdraw_abc123",
"amount": "100.00",
"status": "failed",
"failureReason": "Saldo insuficiente",
"createdAt": 1705312200000
},
"timestamp": 1705312210000
}
A Vision Wallet envia os seguintes headers em cada requisição de webhook:
| Header | Descrição |
|---|
X-Webhook-Signature | Assinatura HMAC-SHA256 do payload (formato: sha256=...) |
X-Webhook-Timestamp | Timestamp Unix em segundos da requisição |
Content-Type | application/json |
User-Agent | Vision-Wallet-API/1.0 |
Segurança e Verificação de Assinatura
IMPORTANTE: Sempre verifique a assinatura do webhook antes de processar qualquer evento. Isso protege seu sistema contra requisições forjadas.
Cada webhook é assinado usando HMAC-SHA256 com sua API Key como secret. Você DEVE verificar a assinatura para garantir que o webhook é autêntico e vem da Vision Wallet.
Como Funciona a Assinatura
- A Vision Wallet cria uma assinatura HMAC-SHA256 do payload JSON usando sua API Key
- A assinatura é enviada no header
X-Webhook-Signature no formato sha256=...
- Você deve recriar a assinatura usando o mesmo método e comparar com a recebida
- Se as assinaturas corresponderem, o webhook é autêntico
Processar Webhooks
Seu endpoint de webhook deve:
- Verificar a assinatura: Validar que a requisição vem da Vision Wallet usando HMAC-SHA256
- Validar o timestamp (opcional): Prevenir ataques de replay verificando se o timestamp não é muito antigo
- Processar o evento: Executar a lógica necessária baseada no tipo de evento
- Retornar 200: Responder com status HTTP 200 para confirmar recebimento
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Sua API Key da Vision Wallet (armazene em variável de ambiente)
const VISION_WALLET_API_KEY = process.env.VISION_WALLET_API_KEY;
/**
* Verifica a assinatura do webhook
* @param {object} payload - O payload JSON recebido
* @param {string} signature - A assinatura do header X-Webhook-Signature
* @param {string} apiKey - Sua API Key da Vision Wallet
* @returns {boolean} - true se a assinatura é válida
*/
function verifyWebhookSignature(payload, signature, apiKey) {
if (!signature || !apiKey) {
return false;
}
// O payload deve ser serializado exatamente como foi enviado
const payloadString = JSON.stringify(payload);
// Criar assinatura esperada usando HMAC-SHA256
const expectedSignature = crypto
.createHmac('sha256', apiKey)
.update(payloadString)
.digest('hex');
// A assinatura vem no formato "sha256=..."
const expectedSignatureWithPrefix = `sha256=${expectedSignature}`;
// Comparação segura para prevenir timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignatureWithPrefix)
);
}
/**
* Valida o timestamp para prevenir replay attacks
* @param {string} timestamp - Timestamp do header X-Webhook-Timestamp
* @param {number} maxAgeSeconds - Idade máxima em segundos (padrão: 5 minutos)
* @returns {boolean} - true se o timestamp é válido
*/
function validateTimestamp(timestamp, maxAgeSeconds = 300) {
if (!timestamp) {
return false;
}
const timestampNum = parseInt(timestamp, 10);
const now = Math.floor(Date.now() / 1000);
const age = now - timestampNum;
return age >= 0 && age <= maxAgeSeconds;
}
// Endpoint de webhook
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body;
// 1. Verificar assinatura (OBRIGATÓRIO)
if (!verifyWebhookSignature(payload, signature, VISION_WALLET_API_KEY)) {
console.warn('Webhook com assinatura inválida recebido');
return res.status(401).json({
error: 'Invalid signature',
message: 'A assinatura do webhook é inválida'
});
}
// 2. Validar timestamp (RECOMENDADO)
if (!validateTimestamp(timestamp)) {
console.warn('Webhook com timestamp inválido recebido');
return res.status(400).json({
error: 'Invalid timestamp',
message: 'O timestamp do webhook é inválido ou muito antigo'
});
}
// 3. Processar o evento
const { event, data } = payload;
switch (event) {
case 'payment.approved':
// Atualizar status do pedido no seu sistema
await updateOrderStatus(data.txid, 'paid');
console.log(`Pagamento aprovado: ${data.txid} - Valor: R$ ${data.netAmount}`);
break;
case 'payment.expired':
// Notificar usuário sobre pagamento expirado
await notifyUser(data.txid, 'payment_expired');
console.log(`Pagamento expirado: ${data.txid}`);
break;
case 'payment.refunded':
// Processar reembolso no seu sistema
await processRefund(data.txid, data.amount);
console.log(`Pagamento reembolsado: ${data.txid}`);
break;
case 'withdraw.completed':
case 'withdrawal.completed':
// Registrar saque processado
await logWithdraw(data.txid, data.sent);
console.log(`Saque processado: ${data.txid} - Valor enviado: R$ ${data.sent}`);
break;
case 'withdraw.failed':
case 'withdrawal.failed':
// Reverter saldo ou notificar sobre falha
await handleWithdrawFailure(data.txid, data.failureReason);
console.log(`Saque falhou: ${data.txid} - Motivo: ${data.failureReason || 'Desconhecido'}`);
break;
default:
console.warn(`Evento desconhecido recebido: ${event}`);
}
// 4. Sempre retornar 200 para confirmar recebimento
res.status(200).json({ received: true });
} catch (error) {
console.error('Erro ao processar webhook:', error);
// Ainda retornar 200 para evitar retentativas desnecessárias
// Mas logue o erro para investigação
res.status(200).json({ received: true, error: error.message });
}
});
// Funções auxiliares (implemente conforme sua lógica de negócio)
async function updateOrderStatus(txid, status) {
// Sua lógica aqui - atualizar pedido como pago
// Exemplo: await db.orders.update({ paymentId: txid }, { status: 'paid' });
}
async function notifyUser(txid, reason) {
// Sua lógica aqui - notificar usuário sobre mudança de status
// Exemplo: await sendEmail(userId, `Pagamento ${reason}`);
}
async function processRefund(txid, amount) {
// Sua lógica aqui - processar reembolso
// Exemplo: await db.refunds.create({ txid, amount, processedAt: new Date() });
}
async function logWithdraw(txid, amount) {
// Sua lógica aqui - registrar saque processado
// Exemplo: await db.withdraws.update({ txid }, { status: 'completed', sent: amount });
}
async function handleWithdrawFailure(txid, reason) {
// Sua lógica aqui - reverter saldo ou notificar sobre falha
// Exemplo: await db.withdraws.update({ txid }, { status: 'failed', failureReason: reason });
}
app.listen(3000, () => {
console.log('Servidor rodando na porta 3000');
});
Boas Práticas de Segurança
NUNCA processe webhooks sem verificar a assinatura! Qualquer pessoa que descobrir sua URL de webhook pode enviar requisições falsas. A única proteção é a verificação da assinatura HMAC-SHA256.
Verificação de Assinatura: Sempre verifique a assinatura usando sua API Key antes de processar qualquer evento. Isso garante que o webhook realmente vem da Vision Wallet.
Validação de Timestamp: Valide o timestamp para prevenir replay attacks. Rejeite webhooks com timestamp muito antigo (recomendado: máximo 5 minutos).
Comparação Segura: Use funções de comparação segura (timing-safe) para comparar assinaturas e prevenir timing attacks:
- Node.js:
crypto.timingSafeEqual()
- Python:
hmac.compare_digest()
- PHP:
hash_equals()
Armazenamento Seguro: Nunca exponha sua API Key no código. Use variáveis de ambiente ou serviços de gerenciamento de secrets.
Boas Práticas de Implementação
Idempotência: Seu webhook deve ser idempotente. O mesmo evento pode ser enviado múltiplas vezes em caso de retentativas. Use IDs únicos para evitar processamento duplicado.
Processamento Assíncrono: Processe eventos de forma assíncrona para responder rapidamente ao servidor. Retorne 200 imediatamente e processe o evento em background.
Logging: Registre todos os eventos recebidos (incluindo tentativas de webhooks inválidos) para auditoria e debugging. Isso ajuda a identificar tentativas de ataque.
Timeout: Configure timeout adequado no seu servidor. A Vision Wallet espera resposta em até 10 segundos antes de considerar como falha.
Tratamento de Erros: Sempre retorne status HTTP 200 mesmo em caso de erro interno. Isso evita retentativas desnecessárias. Logue os erros internamente para investigação.
Se seu webhook não responder com 200 dentro de 10 segundos, a Vision Wallet tentará reenviar o evento usando backoff exponencial (até 5 tentativas). Certifique-se de que seu endpoint está sempre disponível e responde rapidamente.
Testar Webhooks Localmente
Para testar webhooks localmente durante desenvolvimento, você pode usar ferramentas como:
- ngrok: Expõe seu servidor local para a internet (
ngrok http 3000)
- localtunnel: Alternativa ao ngrok (
lt --port 3000)
- webhook.site: Serviço temporário para receber webhooks (útil para ver a estrutura)
Testando a Verificação de Assinatura
Para testar se sua verificação de assinatura está funcionando corretamente, você pode criar um script de teste:
// test-webhook-signature.js
const crypto = require('crypto');
const apiKey = 'sua_api_key_aqui';
const payload = {
event: 'payment.completed',
data: { id: 'test_123' },
timestamp: Date.now()
};
const payloadString = JSON.stringify(payload);
const signature = crypto
.createHmac('sha256', apiKey)
.update(payloadString)
.digest('hex');
console.log('Payload:', payloadString);
console.log('Signature:', `sha256=${signature}`);
console.log('\nUse estes valores para testar seu endpoint:');
console.log('Header X-Webhook-Signature:', `sha256=${signature}`);
console.log('Header X-Webhook-Timestamp:', Math.floor(Date.now() / 1000));
Troubleshooting
Webhook retorna 401 (Invalid signature)
- Verifique se está usando a API Key correta (a mesma usada para configurar o webhook)
- Certifique-se de que o payload está sendo serializado exatamente como recebido (sem alterar ordem de chaves ou espaços)
- Verifique se está comparando a assinatura completa incluindo o prefixo
sha256=
Webhook retorna 400 (Invalid timestamp)
- Verifique se o relógio do servidor está sincronizado
- Ajuste o
maxAgeSeconds se necessário (padrão recomendado: 300 segundos = 5 minutos)
Webhook não está sendo recebido
- Verifique se a URL está acessível publicamente (não localhost)
- Verifique logs do servidor para erros
- Confirme que o endpoint retorna status 200
- Verifique se há firewall bloqueando requisições da Vision Wallet
Consulte a seção API Keys para mais informações sobre como configurar webhooks nas suas API Keys.