Skip to content

Configuração de Webhooks

Instruções para configurar e testar o webhook do ASAAS em sandbox e produção.


Visão Geral

O ASAAS usa webhooks para notificar o sistema sobre eventos de pagamento assíncronos. O endpoint no fast_deliv é: POST /api/v1/webhooks/asaas

ASAAS (evento de transferência)
       │ POST /api/v1/webhooks/asaas
       │ Header: asaas-signature: <hmac-sha256>
Backend FastAPI
       │ 1. Valida HMAC
       │ 2. Identifica evento (TRANSFER_DONE, TRANSFER_FAILED, etc.)
       │ 3. Atualiza withdrawal.status no Supabase
Supabase (withdrawal atualizado)
       │ Realtime broadcast
Driver App (status do saque atualizado)

Configuração no ASAAS Sandbox

1. Acessar configurações de webhook

  1. Entre em sandbox.asaas.com
  2. Vá em Configurações > Integrações > Webhooks
  3. Clique em Adicionar webhook

2. Configurar o webhook

Campo Valor
URL https://fast-deliv-backend.vercel.app/api/v1/webhooks/asaas
Versão v3
Habilitado Sim
Eventos Selecione todos os eventos de transferência

Eventos relevantes: - TRANSFER_DONE - TRANSFER_APPROVED - TRANSFER_FAILED - TRANSFER_CANCELLED

3. Copiar o Token/Secret

Após criar o webhook, copie o Token de autenticação gerado pelo ASAAS. Este valor vai para ASAAS_WEBHOOK_SECRET no backend.


Configuração em Produção

O processo é idêntico ao sandbox, mas: - URL: https://fast-deliv-backend.vercel.app/api/v1/webhooks/asaas - Dashboard: asaas.com (não sandbox) - Gera um novo secret diferente do sandbox


Implementação da Validação

# backend/app/api/v1/webhooks.py
import hashlib
import hmac

@router.post("/asaas", status_code=status.HTTP_200_OK)
async def asaas_webhook(
    request: Request,
    asaas_signature: str | None = Header(default=None, alias="asaas-signature"),
) -> dict[str, str]:
    body = await request.body()

    # Validar assinatura HMAC-SHA256
    if asaas_signature:
        expected = hmac.new(
            settings.asaas_webhook_secret.encode(),
            body,
            hashlib.sha256,
        ).hexdigest()
        if not hmac.compare_digest(expected, asaas_signature):
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Assinatura inválida",
            )

    payload = await request.json()
    event: str = payload.get("event", "")
    transfer_data: dict = payload.get("transfer", {})
    transfer_id = str(transfer_data.get("id", ""))

    if event in ("TRANSFER_DONE", "TRANSFER_APPROVED"):
        supabase.table("withdrawals").update({"status": "completed"}).eq(
            "asaas_transfer_id", transfer_id
        ).execute()

    elif event in ("TRANSFER_FAILED", "TRANSFER_CANCELLED"):
        supabase.table("withdrawals").update({
            "status": "failed",
            "error_message": event,
        }).eq("asaas_transfer_id", transfer_id).execute()

    return {"status": "ok"}

Testando Webhooks Localmente

Para testar webhooks em desenvolvimento, use ngrok ou Cloudflare Tunnel para expor o servidor local.

Com ngrok

# Instalar ngrok
npm install -g ngrok

# Em um terminal: rodar o backend
uv run uvicorn app.main:app --reload --port 8000

# Em outro terminal: criar túnel
ngrok http 8000

# Saída:
# Forwarding  https://abc123.ngrok.io -> http://localhost:8000

Configure o webhook ASAAS sandbox com a URL ngrok:

https://abc123.ngrok.io/api/v1/webhooks/asaas

Com Cloudflare Tunnel (alternativa gratuita)

# Instalar cloudflared
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb

# Criar túnel temporário
cloudflared tunnel --url http://localhost:8000

Simulando Webhooks Manualmente

Para testar o endpoint sem configurar o ASAAS:

# Gerar assinatura HMAC correta
WEBHOOK_SECRET="seu_webhook_secret_aqui"
BODY='{"event":"TRANSFER_DONE","transfer":{"id":"tra_000012345","value":100.00,"status":"DONE"}}'

SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')

# Enviar requisição
curl -X POST http://localhost:8000/api/v1/webhooks/asaas \
  -H "Content-Type: application/json" \
  -H "asaas-signature: $SIGNATURE" \
  -d "$BODY"

# Resposta esperada:
# {"status": "ok"}

Simulando falha

BODY='{"event":"TRANSFER_FAILED","transfer":{"id":"tra_000012345"}}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')

curl -X POST http://localhost:8000/api/v1/webhooks/asaas \
  -H "Content-Type: application/json" \
  -H "asaas-signature: $SIGNATURE" \
  -d "$BODY"

Verificando o Processamento

Após enviar um webhook, verifique o estado do saque no Supabase:

-- No SQL Editor do Supabase
SELECT id, status, asaas_transfer_id, completed_at, error_message
FROM withdrawals
WHERE asaas_transfer_id = 'tra_000012345';

Eventos ASAAS e Ações

Evento Status Final Ação
TRANSFER_DONE completed Pix enviado com sucesso
TRANSFER_APPROVED completed Transferência aprovada
TRANSFER_FAILED failed Falha na transferência
TRANSFER_CANCELLED failed Transferência cancelada
Outros eventos Ignorados (retorna {"status": "ok"})

Troubleshooting

Erro 401: "Assinatura inválida"

  1. Verifique se ASAAS_WEBHOOK_SECRET no .env é exatamente igual ao configurado no ASAAS
  2. Confirme que o body não foi modificado antes da validação
  3. Use hmac.compare_digest() (timing-safe) — nunca ==

Webhook não chegando

  1. Verifique se a URL está acessível publicamente (use ngrok em dev)
  2. Confirme que o endpoint retorna 200 OK — ASAAS faz retry em caso de falha
  3. Verifique os logs de entrega no dashboard ASAAS

Saque não atualizado após TRANSFER_DONE

  1. Confirme que asaas_transfer_id no webhook bate com o registro em withdrawals
  2. Verifique se o transfer_id foi salvo corretamente após a chamada ASAAS
  3. Consulte os logs do backend para erros de banco de dados

Retentativas ASAAS

O ASAAS faz retentativas em caso de falha: - Primeira tentativa: imediata - Segunda tentativa: 5 minutos depois - Terceiras tentativas: 30 minutos, 2 horas, 6 horas

O endpoint deve retornar 200 OK para sinalizar que o webhook foi processado. Retornos de erro causam retentativas mesmo que o evento já tenha sido processado.

Por isso, o processamento é idempotente — processar o mesmo evento duas vezes não causa dano:

# Atualizar para 'completed' se já for 'completed' é inofensivo
supabase.table("withdrawals").update({"status": "completed"}).eq(
    "asaas_transfer_id", transfer_id
).execute()