0Pricing
AI Agents · Aula

Conectando agentes a webhooks

Recebimento de eventos de webhook e acionamento de fluxos de trabalho de agentes em resposta.

Conectando agentes a webhooks é uma aula grátis de AI Agents no CoddyKit. Esta é a aula 2 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de AI Agents, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de AI Agents inclui 4 aulas no total.

O que é um retorno de chamada HTTP?

Um retorno de chamada HTTP é uma chamada de retorno HTTP. Quando ocorre um evento em um serviço externo, ele envia uma solicitação POST ao seu endpoint com os dados do evento. Seu agente processa a carga útil e age.

Os retornos de chamada HTTP são baseados em envio (os eventos chegam quando acontecem), ao contrário das consultas periódicas (nas quais você verifica repetidamente).

Endpoint de retorno de chamada HTTP do FastAPI

O FastAPI facilita a criação de um receptor de retornos de chamada HTTP. Defina uma rota POST, analise o corpo JSON e encaminhe-o à lógica do agente.

from fastapi import FastAPI, Request
from pydantic import BaseModel

app = FastAPI()

class WebhookPayload(BaseModel):
    event: str
    data: dict

@app.post('/webhook')
async def receive_webhook(payload: WebhookPayload):
    print(f'Received event: {payload.event}')
    print(f'Data: {payload.data}')
    
    # Route to the right agent handler
    if payload.event == 'email.received':
        await handle_email_event(payload.data)
    elif payload.event == 'file.uploaded':
        await handle_file_event(payload.data)
    
    return {'status': 'accepted'}

async def handle_email_event(data: dict):
    print(f'Processing email from: {data.get("from")}')

async def handle_file_event(data: dict):
    print(f'Processing file: {data.get("filename")}')

Verificação da assinatura de retornos de chamada HTTP

Sempre verifique se as solicitações de retorno de chamada HTTP vêm do remetente esperado. A maioria dos serviços assina suas cargas úteis com HMAC-SHA256 usando um segredo compartilhado. Rejeite solicitações com assinaturas inválidas.

import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
WEBHOOK_SECRET = 'your-webhook-secret-here'

def verify_signature(payload_bytes: bytes, signature_header: str) -> bool:
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload_bytes,
        hashlib.sha256
    ).hexdigest()
    received = signature_header.replace('sha256=', '')
    return hmac.compare_digest(expected, received)

@app.post('/webhook/verified')
async def verified_webhook(request: Request):
    payload_bytes = await request.body()
    signature = request.headers.get('X-Signature', '')
    
    if not verify_signature(payload_bytes, signature):
        raise HTTPException(status_code=401, detail='Invalid signature')
    
    # Safe to process
    import json
    data = json.loads(payload_bytes)
    return {'status': 'verified', 'event': data.get('event')}

Chaves de idempotência

Os serviços externos costumam tentar novamente as entregas de retornos de chamada HTTP que falharam. Uma chave de idempotência é um ID exclusivo enviado com cada evento. Armazene as chaves processadas e ignore as duplicatas.

from fastapi import FastAPI, Request, HTTPException
import redis
import json

app = FastAPI()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

@app.post('/webhook/idempotent')
async def idempotent_webhook(request: Request):
    payload = await request.json()
    
    # Extract idempotency key from header or payload
    idempotency_key = request.headers.get('Idempotency-Key') or payload.get('event_id')
    
    if not idempotency_key:
        raise HTTPException(status_code=400, detail='Missing idempotency key')
    
    redis_key = f'webhook:processed:{idempotency_key}'
    
    # Check if already processed
    if r.exists(redis_key):
        print(f'Duplicate event {idempotency_key}, skipping')
        return {'status': 'duplicate', 'idempotency_key': idempotency_key}
    
    # Process event
    # ... agent logic here ...
    
    # Mark as processed (expire after 24h)
    r.setex(redis_key, 86400, '1')
    return {'status': 'processed', 'idempotency_key': idempotency_key}

Estratégia de eliminação de duplicatas em novas tentativas

Além das chaves de idempotência, considere janelas de eliminação de duplicatas. Se você receber o mesmo conteúdo de evento dentro de um intervalo curto, provavelmente se trata de uma nova tentativa. Compare os hashes dos eventos para detectar e descartar novas tentativas.

import hashlib
import json
from datetime import datetime

# In-memory store; use Redis in production
recent_hashes = {}
DEDUP_WINDOW_SECONDS = 300  # 5 minutes

def is_duplicate(payload: dict) -> bool:
    # Hash the event content
    content = json.dumps(payload, sort_keys=True)
    event_hash = hashlib.md5(content.encode()).hexdigest()
    
    now = datetime.utcnow().timestamp()
    
    # Clean up old entries
    expired = [h for h, ts in recent_hashes.items() if now - ts > DEDUP_WINDOW_SECONDS]
    for h in expired:
        del recent_hashes[h]
    
    if event_hash in recent_hashes:
        return True
    
    recent_hashes[event_hash] = now
    return False

# Test
payload = {'event': 'payment.completed', 'amount': 100}
print('First:', is_duplicate(payload))   # False
print('Second:', is_duplicate(payload))  # True (duplicate)

Execução assíncrona do agente

Os manipuladores de retornos de chamada HTTP devem responder rapidamente (em menos de 5 segundos) e processar a lógica do agente em segundo plano. Use BackgroundTasks no FastAPI para evitar exceder os limites de tempo.

from fastapi import FastAPI, BackgroundTasks
import asyncio

app = FastAPI()

async def run_agent_job(event: str, data: dict):
    print(f'Agent starting for event: {event}')
    await asyncio.sleep(2)  # Simulate LLM call
    print(f'Agent finished for event: {event}')

@app.post('/webhook/async')
async def async_webhook(request_data: dict, background_tasks: BackgroundTasks):
    event = request_data.get('event', 'unknown')
    data = request_data.get('data', {})
    
    # Respond immediately
    background_tasks.add_task(run_agent_job, event, data)
    
    return {'status': 'accepted', 'message': 'Processing in background'}

Análise de cargas úteis complexas

Serviços diferentes enviam estruturas de carga útil diferentes. Escreva funções de análise dedicadas para cada serviço, para que seu agente sempre receba um objeto de evento normalizado.

from dataclasses import dataclass
from typing import Optional

@dataclass
class NormalizedEvent:
    event_type: str
    source: str
    resource_id: str
    metadata: dict

def parse_github_webhook(payload: dict) -> NormalizedEvent:
    return NormalizedEvent(
        event_type='github.' + payload.get('action', 'unknown'),
        source='github',
        resource_id=str(payload.get('repository', {}).get('id', '')),
        metadata={
            'repo': payload.get('repository', {}).get('full_name'),
            'sender': payload.get('sender', {}).get('login')
        }
    )

def parse_stripe_webhook(payload: dict) -> NormalizedEvent:
    return NormalizedEvent(
        event_type=payload.get('type', 'unknown'),
        source='stripe',
        resource_id=payload.get('id', ''),
        metadata={'amount': payload.get('data', {}).get('object', {}).get('amount')}
    )

# Usage
github_payload = {'action': 'opened', 'repository': {'id': 123, 'full_name': 'user/repo'}, 'sender': {'login': 'alice'}}
event = parse_github_webhook(github_payload)
print(event)

Os códigos de resposta dos retornos HTTP são importantes

Retorne o status HTTP correto. Um código 2xx informa ao remetente que o retorno de chamada HTTP foi aceito. Um código 4xx indica um erro do cliente (carga útil inválida). Um código 5xx ou o estouro do limite de tempo faz o remetente tentar novamente.

  • 200: Aceito e processado
  • 202: Aceito para processamento assíncrono
  • 400: Solicitação inválida (campos ausentes)
  • 401: Assinatura inválida
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse

app = FastAPI()

@app.post('/webhook/proper-responses')
async def proper_webhook(request: Request):
    try:
        payload = await request.json()
    except Exception:
        raise HTTPException(status_code=400, detail='Invalid JSON body')
    
    required_fields = ['event', 'data']
    for field in required_fields:
        if field not in payload:
            raise HTTPException(status_code=400, detail=f'Missing field: {field}')
    
    event = payload['event']
    known_events = ['email.received', 'file.uploaded', 'payment.completed']
    
    if event not in known_events:
        # Acknowledge unknown events gracefully - do not retry
        return JSONResponse(status_code=200, content={'status': 'ignored', 'reason': 'unknown event'})
    
    # Start background processing
    return JSONResponse(status_code=202, content={'status': 'accepted'})

Testando retornos de chamada HTTP localmente

Use o ngrok para expor seu servidor local à internet para testes. Execute ngrok http 8000 para obter uma URL pública que cria um túnel até seu aplicativo local do FastAPI.

# Start your FastAPI app
# uvicorn main:app --reload --port 8000

# In another terminal, start ngrok:
# ngrok http 8000
# You get: https://abc123.ngrok.io

# Now configure your webhook in Stripe/GitHub/etc. to:
# https://abc123.ngrok.io/webhook

# Test with curl:
import subprocess

def test_webhook_locally():
    test_payload = '{"event": "email.received", "data": {"from": "test@example.com"}}'
    # In real usage you would run this in terminal:
    # curl -X POST http://localhost:8000/webhook \
    #   -H 'Content-Type: application/json' \
    #   -d '{"event": "email.received", "data": {"from": "test@example.com"}}'
    print('Test payload:', test_payload)
    print('Send to: http://localhost:8000/webhook')

test_webhook_locally()

Registro de eventos de retornos de chamada HTTP

Registre cada retorno de chamada HTTP recebido com timestamp, origem, tipo de evento e resultado do processamento. Essa trilha de auditoria é essencial para depurar eventos perdidos ou problemas de processamento duplicado.

import logging
import json
from datetime import datetime
import sys

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(levelname)s %(message)s',
    stream=sys.stdout
)
logger = logging.getLogger('webhook')

def log_webhook_event(event_id: str, event_type: str, source: str, status: str, details: dict = None):
    logger.info(json.dumps({
        'timestamp': datetime.utcnow().isoformat(),
        'event_id': event_id,
        'event_type': event_type,
        'source': source,
        'status': status,
        'details': details or {}
    }))

# Usage in webhook handler
log_webhook_event(
    event_id='evt_123',
    event_type='email.received',
    source='gmail',
    status='processed',
    details={'from': 'user@example.com', 'action_taken': 'reply_sent'}
)

Limitação de taxa para retornos de chamada HTTP recebidos

Proteja seu endpoint de retorno de chamada HTTP contra sobrecarga usando limitação de taxa. A biblioteca slowapi adiciona limitação de taxa ao FastAPI com poucas linhas de código.

from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.post('/webhook/limited')
@limiter.limit('100/minute')
async def rate_limited_webhook(request: Request):
    payload = await request.json()
    return {'status': 'accepted', 'event': payload.get('event')}

Verificação de conhecimentos: retornos de chamada HTTP

Teste sua compreensão das práticas recomendadas para agentes que usam retornos de chamada HTTP.

Retornos de chamada HTTP em produção

Em produção, combine todos os padrões: verificação de assinatura, chaves de idempotência, processamento em segundo plano, registro estruturado e limitação de taxa. Implante atrás de um proxy reverso como o nginx para a terminação de TLS e proteção adicional.

Perguntas Frequentes

A aula “Conectando agentes a webhooks” é grátis?

Sim — o texto completo de “Conectando agentes a webhooks” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de AI Agents, atualize para CoddyKit PRO. O curso de AI Agents inclui 4 aulas no total.

O que vou aprender em “Conectando agentes a webhooks”?

Recebimento de eventos de webhook e acionamento de fluxos de trabalho de agentes em resposta. Você pratica AI Agents com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar AI Agents?

Nenhuma experiência prévia é necessária. AI Agents no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 2 de 4.

Quanto tempo leva a aula “Conectando agentes a webhooks”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de AI Agents?

Sim. Cada aula de AI Agents inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Padrões de agentes baseados em gatilho e ação
  2. Conectando agentes a webhooks
  3. Agentes programados e baseados em Cron
  4. Construindo um pipeline de automação entre vários aplicativos
← Voltar para AI Agents