Configuração
Para receber webhooks, você deve fornecer o campo postbackUrl ao criar um saque através da API:
{
"pixkeytype": "CPF",
"pixkey": "99999999999",
"requestedamount": 254,
"description": "Saque via API",
"isPix": true,
"postbackUrl": "https://seu-dominio.com/webhook/withdrawals"
}
Requisitos do Endpoint
Seu endpoint de webhook deve:
- ✅ Aceitar requisições POST
- ✅ Responder com status HTTP 200 para confirmar recebimento
- ✅ Processar o payload JSON enviado no body
- ✅ Responder em até 30 segundos
- ✅ Usar HTTPS
Eventos Disponíveis
📋 Lista de Eventos
| Evento | Descrição | Quando é enviado |
|---|---|---|
withdrawal.created | Saque criado com sucesso | Imediatamente após criação bem-sucedida |
withdrawal.status_changed | Status do saque foi alterado | Quando há mudança de status (ex: pending → approved) |
withdrawal.completed | Saque foi concluído | Quando o saque é finalizado com sucesso |
withdrawal.failed | Saque falhou | Quando o saque falha ou é cancelado |
🔄 Fluxo Típico de Eventos
graph LR
A[withdrawal.created] --> B[withdrawal.status_changed]
B --> C[withdrawal.completed]
B --> D[withdrawal.failed]
Exemplo de sequência:
withdrawal.created- Status:pendingwithdrawal.status_changed- Status:approvedwithdrawal.status_changed- Status:processingwithdrawal.completed- Status:done
Status de Saques
📊 Todos os Status Possíveis
| Status | Descrição | Evento Típico |
|---|---|---|
pending | Aguardando aprovação manual | withdrawal.created |
approved | Aprovado para processamento | withdrawal.status_changed |
processing | Em processamento pelo BaaS | withdrawal.status_changed |
done | Concluído automaticamente | withdrawal.completed |
done_manual | Concluído manualmente | withdrawal.completed |
failed | Falhou durante processamento | withdrawal.failed |
refused | Recusado na aprovação | withdrawal.failed |
cancelled | Cancelado pelo sistema | withdrawal.failed |
🔀 Transições de Status
pending → approved → processing → done
pending → refused
approved → failed
processing → failed
pending → cancelled
Formato do Payload
Estrutura Base
Todos os webhooks seguem a mesma estrutura base:
{
"event": "withdrawal.created",
"timestamp": "2025-07-10T17:40:27.373Z",
"withdrawal": {
// Dados completos do saque
},
"metadata": {
"source": "withdrawals_service",
"version": "1.0.0"
}
}
Objeto Withdrawal
{
"id": "756d4eec-9a22-44b0-a514-a27c366c5433",
"company_id": "5e1ce642-b9f1-433c-a350-5f9cd3d16bf6",
"requested_amount": 2.54,
"currency": "BRL",
"status": "pending",
"created_at": "2025-07-10T14:40:26.270543",
"updated_at": "2025-07-10T14:40:26.270543",
"paid_at": null,
"pix": {
"key_type": "CPF",
"key_value": "99999999999",
"end_to_end_id": null
},
"fee": 0,
"net_amount": 2.54,
"error_message": null
}
Exemplos de Webhooks
1. withdrawal.created
Enviado quando um saque é criado com sucesso.
{
"event": "withdrawal.created",
"timestamp": "2025-07-10T17:40:27.373Z",
"withdrawal": {
"id": "756d4eec-9a22-44b0-a514-a27c366c5433",
"company_id": "5e1ce642-b9f1-433c-a350-5f9cd3d16bf6",
"requested_amount": 2.54,
"currency": "BRL",
"status": "pending",
"created_at": "2025-07-10T14:40:26.270543",
"updated_at": "2025-07-10T14:40:26.270543",
"paid_at": null,
"pix": {
"key_type": "CPF",
"key_value": "99999999999",
"end_to_end_id": null
},
"fee": 0,
"net_amount": 2.54,
"error_message": null
},
"metadata": {
"source": "withdrawals_service",
"version": "1.0.0"
}
}
2. withdrawal.status_changed
Enviado quando o status de um saque é alterado.
{
"event": "withdrawal.status_changed",
"timestamp": "2025-07-10T17:45:12.123Z",
"withdrawal": {
"id": "756d4eec-9a22-44b0-a514-a27c366c5433",
"company_id": "5e1ce642-b9f1-433c-a350-5f9cd3d16bf6",
"requested_amount": 2.54,
"currency": "BRL",
"status": "approved",
"created_at": "2025-07-10T14:40:26.270543",
"updated_at": "2025-07-10T17:45:12.100000",
"paid_at": null,
"pix": {
"key_type": "CPF",
"key_value": "99999999999",
"end_to_end_id": null
},
"fee": 0,
"net_amount": 2.54,
"error_message": null
},
"metadata": {
"source": "withdrawals_service",
"version": "1.0.0"
}
}
3. withdrawal.completed
Enviado quando um saque é concluído com sucesso.
{
"event": "withdrawal.completed",
"timestamp": "2025-07-10T18:15:45.456Z",
"withdrawal": {
"id": "756d4eec-9a22-44b0-a514-a27c366c5433",
"company_id": "5e1ce642-b9f1-433c-a350-5f9cd3d16bf6",
"requested_amount": 2.54,
"currency": "BRL",
"status": "done",
"created_at": "2025-07-10T14:40:26.270543",
"updated_at": "2025-07-10T18:15:45.400000",
"paid_at": "2025-07-10T18:15:45.400000",
"pix": {
"key_type": "CPF",
"key_value": "99999999999",
"end_to_end_id": "E1234567890123456789012345678901"
},
"fee": 0,
"net_amount": 2.54,
"error_message": null
},
"metadata": {
"source": "withdrawals_service",
"version": "1.0.0"
}
}
4. withdrawal.failed
Enviado quando um saque falha ou é cancelado.
{
"event": "withdrawal.failed",
"timestamp": "2025-07-10T18:20:30.789Z",
"withdrawal": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"company_id": "5e1ce642-b9f1-433c-a350-5f9cd3d16bf6",
"requested_amount": 100.0,
"currency": "BRL",
"status": "failed",
"created_at": "2025-07-10T18:10:15.123456",
"updated_at": "2025-07-10T18:20:30.750000",
"paid_at": null,
"pix": {
"key_type": "EMAIL",
"key_value": "[email protected]",
"end_to_end_id": null
},
"fee": 2.5,
"net_amount": 97.5,
"error_message": "Chave PIX não encontrada no sistema do banco"
},
"metadata": {
"source": "withdrawals_service",
"version": "1.0.0"
}
}
Implementação
Exemplo básico (Node.js/Express)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/withdrawals', (req, res) => {
const { event, withdrawal, timestamp } = req.body;
console.log(`[${timestamp}] Evento recebido: ${event}`);
console.log(`Saque ID: ${withdrawal.id}, Status: ${withdrawal.status}`);
try {
switch (event) {
case 'withdrawal.created':
handleWithdrawalCreated(withdrawal);
break;
case 'withdrawal.status_changed':
handleStatusChanged(withdrawal);
break;
case 'withdrawal.completed':
handleWithdrawalCompleted(withdrawal);
break;
case 'withdrawal.failed':
handleWithdrawalFailed(withdrawal);
break;
default:
console.log(`Evento desconhecido: ${event}`);
}
// IMPORTANTE: Sempre responder com 200
res.status(200).send('OK');
} catch (error) {
console.error('Erro ao processar webhook:', error);
res.status(200).send('OK'); // Ainda assim responder 200 para evitar retry
}
});
function handleWithdrawalCreated(withdrawal) {
console.log('Novo saque criado:', withdrawal.id);
// Salvar no banco, enviar notificação, etc.
}
function handleStatusChanged(withdrawal) {
console.log(`Status alterado para: ${withdrawal.status}`);
// Atualizar status no seu sistema
}
function handleWithdrawalCompleted(withdrawal) {
console.log('Saque concluído!', withdrawal.id);
// Confirmar pagamento, atualizar saldo, etc.
}
function handleWithdrawalFailed(withdrawal) {
console.log('Saque falhou:', withdrawal.error_message);
// Tratar erro, reverter operações, notificar usuário
}
app.listen(3000, () => {
console.log('Servidor webhook rodando na porta 3000');
});
Exemplo com validação (Python/Flask)
from flask import Flask, request, jsonify
import json
import logging
from datetime import datetime
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/webhook/withdrawals', methods=['POST'])
def handle_withdrawal_webhook():
try:
# Validar content-type
if request.content_type != 'application/json':
logging.warning('Content-type inválido')
return 'OK', 200
payload = request.get_json()
# Validar estrutura básica
required_fields = ['event', 'timestamp', 'withdrawal', 'metadata']
if not all(field in payload for field in required_fields):
logging.error('Payload inválido - campos obrigatórios ausentes')
return 'OK', 200
event = payload['event']
withdrawal = payload['withdrawal']
timestamp = payload['timestamp']
logging.info(f"[{timestamp}] Evento: {event}, Saque: {withdrawal['id']}")
# Processar evento
if event == 'withdrawal.created':
handle_withdrawal_created(withdrawal)
elif event == 'withdrawal.status_changed':
handle_status_changed(withdrawal)
elif event == 'withdrawal.completed':
handle_withdrawal_completed(withdrawal)
elif event == 'withdrawal.failed':
handle_withdrawal_failed(withdrawal)
else:
logging.warning(f"Evento desconhecido: {event}")
return 'OK', 200
except Exception as e:
logging.error(f"Erro ao processar webhook: {e}")
return 'OK', 200 # Sempre retornar 200 para evitar retry
def handle_withdrawal_created(withdrawal):
logging.info(f"Novo saque criado: {withdrawal['id']}")
# Implementar lógica específica
def handle_status_changed(withdrawal):
logging.info(f"Status alterado para: {withdrawal['status']}")
# Atualizar banco de dados local
def handle_withdrawal_completed(withdrawal):
logging.info(f"Saque concluído: {withdrawal['id']}")
# Confirmar operação, notificar usuário
def handle_withdrawal_failed(withdrawal):
error_msg = withdrawal.get('error_message', 'Erro desconhecido')
logging.error(f"Saque falhou: {error_msg}")
# Tratar erro, reverter operações
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Política de Retry
Como funciona
- Tentativas: Até 3 tentativas de entrega
- Intervalos: 1 minuto, 5 minutos, 15 minutos
- Condição para retry: Status HTTP diferente de 200
- Timeout: 30 segundos por tentativa
Sequência de retry
Tentativa 1: Imediato
↓ (falha)
Tentativa 2: +1 minuto
↓ (falha)
Tentativa 3: +5 minutos
↓ (falha)
Tentativa 4: +15 minutos
↓ (falha)
Webhook descartado
Evitando retries desnecessários
// ✅ CORRETO - Sempre responder 200
app.post('/webhook', (req, res) => {
try {
processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
console.error('Erro:', error);
res.status(200).send('OK'); // Ainda assim responder 200
}
});
// ❌ INCORRETO - Causará retry
app.post('/webhook', (req, res) => {
try {
processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
res.status(500).send('Erro interno'); // Causará retry!
}
});
Troubleshooting
Problemas Comuns
| Problema | Causa | Solução |
|---|---|---|
| Webhooks não chegam | URL inacessível | Verificar se URL responde e usa HTTPS |
| Múltiplos retries | Não responde 200 | Sempre responder com status 200 |
| Payloads duplicados | Processamento lento | Implementar idempotência usando withdrawal.id |
| Timeout | Processamento > 30s | Processar de forma assíncrona |
Última atualização: 10/07/2025
