Webhooks
Receba notificações em tempo real quando os pagamentos são concluídos, falham ou estão pendentes.
Visão Geral
Os Webhooks permitem ao seu servidor receber notificações automáticas HTTP POST quando ocorrem eventos na sua conta Fivo. Em vez de consultar a API, registe um URL e o Fivo envia-lhe os dados do evento assim que acontecem.
Cada pedido de webhook é assinado com HMAC-SHA256 para que possa verificar que veio do Fivo.
Configuração
Configure webhooks a partir da sua página :Painel → Webhooks
- Clique em Adicionar Webhook
- Introduza o URL do seu endpoint (deve ser HTTPS)
- Selecione os eventos que pretende subscrever
- Guarde: o Fivo gera um segredo de assinatura para si
Requisitos
- O URL deve usar HTTPS
- O URL não pode apontar para endereços IP privados/internos (proteção SSRF)
- O seu endpoint deve responder com um código 2xx em até 5 segundos
Eventos
| Evento | Descrição |
|---|---|
payment.completed | O pagamento foi confirmado on-chain e creditado na sua wallet |
payment.failed | O pagamento falhou (transação revertida ou expirada) |
refund.created | Um reembolso foi criado para um pagamento |
refund.completed | O reembolso foi concluído on-chain e enviado ao cliente |
refund.failed | O reembolso falhou (verifique failure_reason nos dados) |
Payload
Cada webhook envia um payload JSON com o tipo de evento, um timestamp e os dados do evento:
Eventos de Pagamento
{
"event": "payment.completed",
"timestamp": "2026-03-04T10:30:45.123Z",
"data": {
"payment_id": "fivo_live_abc123",
"amount": "100.00",
"currency": "USDC",
"status": "completed",
"tx_hash": "0xabc...def",
"from_address": "0x1234...5678",
"to_address": "0xabcd...ef01",
"source_chain": "ETH",
"destination_chain": "BASE",
"is_cross_chain": true,
"merchant_transfer_tx_hash": "0x9876...5432",
"customer_email": "customer@example.com",
"reference": "ORD-2026-001",
"metadata": { "product_id": "plan_pro" }
}
}Eventos de Reembolso
{
"event": "refund.completed",
"timestamp": "2026-03-04T10:35:20.456Z",
"data": {
"refund_id": "refund-uuid",
"payment_id": "payment-uuid",
"amount": "49.99",
"currency": "USDC",
"status": "completed",
"destination_address": "0xCustomer...",
"blockchain": "BASE",
"reason": "requested_by_customer",
"tx_hash": "0xabc..."
}
}Campos dos Dados de Pagamento
objeto data (eventos de pagamento)
payment_idstringRequiredID de pagamento Fivo
amountstringRequiredMontante do pagamento (string decimal)
currencystringRequiredSímbolo do token: USDC ou EURC
statusstringRequiredEstado do pagamento: pending, completed ou failed
tx_hashstring | nullRequiredHash da transação on-chain
from_addressstring | nullRequiredEndereço da wallet do pagador
to_addressstring | nullRequiredEndereço da wallet do comerciante
source_chainstring | nullRequiredBlockchain de origem (ex.: ETH, BASE)
destination_chainstring | nullRequiredBlockchain de destino (null para mesma cadeia)
is_cross_chainbooleanRequiredSe o pagamento foi transferido entre cadeias
merchant_transfer_tx_hashstringOptionalHash da transação da transferência para a wallet do comerciante (apenas entre cadeias)
customer_emailstring | nullOptionalEmail do cliente, se fornecido durante o pagamento
referencestring | nullOptionalReferência do comerciante (ex.: ID do pedido)
metadataobject | nullOptionalMetadados personalizados associados ao pagamento
| Name | Type | Required | Description |
|---|---|---|---|
payment_id | string | Required | ID de pagamento Fivo |
amount | string | Required | Montante do pagamento (string decimal) |
currency | string | Required | Símbolo do token: USDC ou EURC |
status | string | Required | Estado do pagamento: pending, completed ou failed |
tx_hash | string | null | Required | Hash da transação on-chain |
from_address | string | null | Required | Endereço da wallet do pagador |
to_address | string | null | Required | Endereço da wallet do comerciante |
source_chain | string | null | Required | Blockchain de origem (ex.: ETH, BASE) |
destination_chain | string | null | Required | Blockchain de destino (null para mesma cadeia) |
is_cross_chain | boolean | Required | Se o pagamento foi transferido entre cadeias |
merchant_transfer_tx_hash | string | Optional | Hash da transação da transferência para a wallet do comerciante (apenas entre cadeias) |
customer_email | string | null | Optional | Email do cliente, se fornecido durante o pagamento |
reference | string | null | Optional | Referência do comerciante (ex.: ID do pedido) |
metadata | object | null | Optional | Metadados personalizados associados ao pagamento |
Cabeçalhos do Pedido
O Fivo inclui estes cabeçalhos em cada pedido de webhook:
Cabeçalhos
X-Fivo-SignaturestringRequiredAssinatura HMAC-SHA256 com prefixo sha256= (ver Verificação de Assinaturas)
X-Fivo-EventstringRequiredTipo de evento (ex.: payment.completed)
X-Fivo-TimestampstringRequiredTimestamp Unix em segundos de quando o pedido foi enviado
X-Fivo-TeststringOptionalDefinido como "true" para webhooks de teste enviados a partir do painel
| Name | Type | Required | Description |
|---|---|---|---|
X-Fivo-Signature | string | Required | Assinatura HMAC-SHA256 com prefixo sha256= (ver Verificação de Assinaturas) |
X-Fivo-Event | string | Required | Tipo de evento (ex.: payment.completed) |
X-Fivo-Timestamp | string | Required | Timestamp Unix em segundos de quando o pedido foi enviado |
X-Fivo-Test | string | Optional | Definido como "true" para webhooks de teste enviados a partir do painel |
Verificação de Assinaturas
Verifique sempre o cabeçalho para confirmar que o pedido veio do Fivo. A assinatura é calculada como usando o seu segredo de webhook como chave, com o prefixo .X-Fivo-SignatureHMAC-SHA256(timestamp + "." + body)sha256=.
O seu segredo de webhook tem este formato: whsec_a1b2c3d4e5f6...
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-fivo-signature'];
const timestamp = req.headers['x-fivo-timestamp'];
const body = JSON.stringify(req.body);
// 1. Reject if timestamp is older than 5 minutes (replay protection)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 5 * 60) {
throw new Error('Webhook timestamp too old');
}
// 2. Compute expected signature: HMAC-SHA256(timestamp + "." + body)
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + body)
.digest('hex');
// 3. Compare using timing-safe comparison
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(body);
}Comparação timing-safe
crypto.timingSafeEqual()===Tentativas e Desativação Automática
Se o seu endpoint devolver um código diferente de 2xx ou expirar, o Fivo incrementa um contador de falhas. Após falhas consecutivas, o webhook é automaticamente desativado para evitar mais entregas falhadas.10
Pode reativar um webhook desativado a partir do painel a qualquer momento. O contador de falhas é reiniciado com qualquer entrega bem-sucedida.
Testes
Use o botão na sua página de para enviar um evento de teste para o seu endpoint. Os webhooks de teste incluem o cabeçalho para que o seu servidor possa distingui-los dos eventos reais.Enviar testeWebhooksX-Fivo-Test: true
Os registos de entrega são visíveis na página de detalhes do webhook: pode inspecionar o payload, o código de resposta e o tempo de resposta de cada entrega.
Exemplo Completo (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.FIVO_WEBHOOK_SECRET;
app.post('/webhooks/fivo', (req, res) => {
try {
// 1. Verify signature
const signature = req.headers['x-fivo-signature'];
const timestamp = req.headers['x-fivo-timestamp'];
const body = JSON.stringify(req.body);
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(timestamp + '.' + body)
.digest('hex');
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Replay protection
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 5 * 60) {
return res.status(401).json({ error: 'Timestamp too old' });
}
// 3. Handle event
const { event, data } = req.body;
switch (event) {
case 'payment.completed':
console.log('Payment received:', data.amount, data.currency);
console.log('From:', data.from_address, 'on', data.source_chain);
console.log('Customer:', data.customer_email);
console.log('Reference:', data.reference);
// Update your order status, send confirmation, etc.
break;
case 'payment.failed':
console.log('Payment failed:', data.payment_id);
break;
case 'refund.created':
console.log('Refund created:', data.refund_id);
break;
case 'refund.completed':
console.log('Refund completed:', data.refund_id);
break;
case 'refund.failed':
console.log('Refund failed:', data.refund_id);
break;
}
res.status(200).json({ received: true });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
app.listen(3000);