Webhooks

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

EventoDescriçãoQuando é enviado
withdrawal.createdSaque criado com sucessoImediatamente após criação bem-sucedida
withdrawal.status_changedStatus do saque foi alteradoQuando há mudança de status (ex: pending → approved)
withdrawal.completedSaque foi concluídoQuando o saque é finalizado com sucesso
withdrawal.failedSaque falhouQuando 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:

  1. withdrawal.created - Status: pending
  2. withdrawal.status_changed - Status: approved
  3. withdrawal.status_changed - Status: processing
  4. withdrawal.completed - Status: done

Status de Saques

📊 Todos os Status Possíveis

StatusDescriçãoEvento Típico
pendingAguardando aprovação manualwithdrawal.created
approvedAprovado para processamentowithdrawal.status_changed
processingEm processamento pelo BaaSwithdrawal.status_changed
doneConcluído automaticamentewithdrawal.completed
done_manualConcluído manualmentewithdrawal.completed
failedFalhou durante processamentowithdrawal.failed
refusedRecusado na aprovaçãowithdrawal.failed
cancelledCancelado pelo sistemawithdrawal.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

ProblemaCausaSolução
Webhooks não chegamURL inacessívelVerificar se URL responde e usa HTTPS
Múltiplos retriesNão responde 200Sempre responder com status 200
Payloads duplicadosProcessamento lentoImplementar idempotência usando withdrawal.id
TimeoutProcessamento > 30sProcessar de forma assíncrona

Última atualização: 10/07/2025