green snake
Photo by Pixabay on Pexels.com
目次

Guía práctica para implementar de forma segura APIs receptoras de webhooks en FastAPI: desde la verificación de firmas y el manejo de reintentos hasta la idempotencia y el procesamiento asíncrono


Resumen

  • Una API receptora de webhooks no es solo “un endpoint que acepta solicitudes POST”. En sistemas reales, es necesario diseñar pensando en la verificación de autenticidad del emisor, la protección contra manipulaciones, la tolerancia a reintentos, la prevención de timeouts y la auditabilidad. Stripe recomienda usar su librería oficial para la verificación de firmas, y GitHub también recomienda firmemente la verificación de firmas mediante un secreto compartido.
  • En FastAPI, el patrón seguro es obtener primero el cuerpo bruto de la solicitud desde Request, verificar la firma antes de cualquier otra cosa y solo después enrutar el evento a la lógica de procesamiento según el tipo de evento. Si modificas el cuerpo antes de la verificación, es muy probable que la validación falle. Stripe también indica explícitamente que se debe usar el cuerpo sin modificar.
  • En producción, suele ser más estable no completar el procesamiento pesado de negocio dentro del propio endpoint del webhook. En su lugar, el enfoque recomendado es verificar, registrar, devolver un ACK rápidamente y delegar el procesamiento posterior a tareas en segundo plano o a una cola de trabajos. BackgroundTasks de FastAPI es adecuado para trabajo ligero posterior a la respuesta.
  • En este artículo, organizaré una implementación segura de webhooks en FastAPI en el siguiente orden: diseño básico → verificación de firmas → idempotencia → manejo de reintentos y desorden en la llegada → registros de auditoría → pruebas, incorporando las ideas de diseño de Stripe y GitHub y presentándolas como un patrón práctico con menor dependencia de un proveedor concreto.

A quién beneficiará leer esto

Desarrolladores individuales y personas que están aprendiendo

  • Personas que quieren recibir eventos de finalización de pago o eventos de GitHub en FastAPI, pero todavía solo tienen una idea vaga de cómo construir webhooks de forma segura.
  • Personas que piensan: “¿No basta con recibir el POST y actualizar la base de datos?”, pero quieren entender qué más hace falta cuando se consideran la verificación de firmas y el manejo de reintentos.

Para estos lectores, el principal valor está en establecer la idea de que un webhook es un punto de entrada autenticado de eventos externos, y luego aprender un patrón que parte de una estructura mínima y evoluciona hacia un diseño más seguro. Tanto Stripe como GitHub enfatizan la verificación de firmas usando un secreto compartido.

Ingenieros backend en equipos pequeños

  • Ingenieros que ya han empezado a lidiar con múltiples webhooks, como pagos, GitHub y eventos de SaaS externos, y cuyo diseño de endpoints y manejo de reintentos se está volviendo disperso.
  • Ingenieros que quieren evitar timeouts y procesamiento duplicado mientras organizan las partes reutilizables.

Para estos lectores, la parte más útil será la separación de responsabilidades entre “recibir, verificar, registrar, ACK, procesamiento posterior”, así como la forma de dividir la verificación de firmas, la persistencia del ID del evento y el despacho de trabajos. Las mejores prácticas generales para webhooks también enfatizan la autenticidad, los reintentos, la idempotencia y las respuestas rápidas.

Equipos SaaS y startups

  • Equipos para los que las integraciones de pagos y eventos externos son críticas para el negocio, y en los que perder eventos o procesarlos dos veces afecta directamente a los ingresos y a la experiencia del cliente.
  • Equipos que quieren construir en FastAPI una base receptora que siga siendo robusta, auditable y tolerante a reintentos incluso cuando el número de tipos de eventos aumente en el futuro.

Para estos lectores, el valor clave está en un diseño en el que la recepción del evento se separa del procesamiento de negocio y se canaliza hacia registros de auditoría, una tabla de idempotencia y una cola de trabajos. Tanto GitHub como Stripe muestran que los webhooks deben manejarse solo después de la verificación de firma y con el supuesto de que los fallos y reintentos son condiciones normales.


Evaluación de accesibilidad

  • Se coloca primero un resumen y luego una estructura paso a paso de “por qué es peligroso”, “cómo protegerlo” y “cómo implementarlo en FastAPI”.
  • Los términos técnicos se explican brevemente en su primera aparición y luego se usan de manera consistente.
  • El código se divide en secciones cortas para que cada parte pueda verse según su función.
  • El nivel objetivo es equivalente a AA.

1. ¿Por qué una API receptora de webhooks debe construirse con más cuidado que una API POST normal?

Un webhook es una notificación que un servicio externo envía activamente a tu servidor. Por ejemplo, eventos como “pago exitoso”, “factura fallida” o “repositorio actualizado” llegan desde fuera. GitHub recomienda que el servidor receptor realice la verificación de firma antes de procesar las entregas del webhook, y Stripe documenta oficialmente la verificación usando el encabezado Stripe-Signature y un secreto del endpoint.

Una API interna normal suele ser invocada por tu propio cliente usando un token de autenticación. Un webhook es muy distinto porque debes verificar de tu lado si el remitente es legítimo. Además, los webhooks pueden reenviarse y pueden llegar fuera de orden, por lo que si escribes tu código bajo las suposiciones de que “llegará una sola vez” y “siempre llegará en orden”, se volverá frágil en operación real. Las mejores prácticas de webhooks insisten una y otra vez en autenticación, firmas, reintentos e idempotencia.


2. El primer principio de diseño: mantener pequeñas las responsabilidades del endpoint receptor

El enfoque más seguro es mantener lo más pequeñas posible las responsabilidades del endpoint receptor de webhooks. Un flujo recomendado es:

  1. Recibir el cuerpo bruto de la solicitud y los encabezados
  2. Verificar la firma y la autenticidad del remitente
  3. Registrar el ID del evento y otros identificadores similares, y comprobar si es un duplicado
  4. Devolver una respuesta 2xx lo más rápido posible cuando corresponda
  5. Delegar el procesamiento de negocio realmente pesado a un sistema posterior

Con esta estructura, el receptor del webhook se convierte en la “puerta de entrada de eventos externos”, claramente separada del cuerpo principal de la lógica de negocio. La documentación de Stripe también explica los endpoints de webhook sobre la base de que primero verifican firmas y luego manejan eventos con seguridad, y FastAPI permite trabajo ligero posterior a la respuesta usando BackgroundTasks.


3. Fundamentos de la verificación de firmas: un secreto más el cuerpo bruto de la solicitud

GitHub documenta la verificación usando X-Hub-Signature-256 con un secreto del webhook. Si se configura un secreto, GitHub incluye un digest HMAC SHA-256 en el encabezado. Stripe hace algo similar, exigiendo el encabezado Stripe-Signature, el secreto del endpoint y el cuerpo bruto e inalterado de la solicitud para la verificación.

El punto especialmente importante aquí es que la verificación debe usar el cuerpo bruto antes de parsearlo como JSON. Stripe identifica explícitamente los cuerpos modificados como causa de errores de verificación de firma y considera el acceso al cuerpo sin modificar como un requisito crítico. En FastAPI, puedes acceder directamente al cuerpo bruto desde el objeto Request.


4. Un ejemplo mínimo en FastAPI para recibir el cuerpo bruto de forma segura

Primero, creemos el esqueleto de un receptor de webhooks en FastAPI. Aquí recibimos los encabezados y el cuerpo bruto de una forma agnóstica al proveedor.

from fastapi import APIRouter, Request, Header, HTTPException, status

router = APIRouter(prefix="/webhooks", tags=["webhooks"])

@router.post("/generic")
async def receive_webhook(
    request: Request,
    x_signature: str | None = Header(default=None),
):
    raw_body = await request.body()

    if not x_signature:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="missing signature header",
        )

    # Realizar aquí la verificación de firma
    # Parsear JSON solo después de que la verificación de firma tenga éxito
    payload = await request.json()

    return {"received": True}

En esta etapa, la verificación de firma aún no está implementada, pero los puntos centrales ya están claros.
Debes llamar primero a await request.body() para obtener el cuerpo bruto, y no debes pasar a la lógica de negocio antes de completar la verificación de firma. La capacidad de acceder al cuerpo bruto de la solicitud mediante el objeto Request de FastAPI es extremadamente importante para las implementaciones de webhooks.


5. Un patrón básico para implementar por cuenta propia la verificación HMAC

Aquí tienes un patrón mínimo de verificación HMAC-SHA256 similar a la verificación de webhooks de GitHub. Si el proveedor real ofrece una librería oficial, deberías preferirla en primer lugar. Stripe también recomienda usar su librería oficial.

import hashlib
import hmac

def verify_hmac_sha256(raw_body: bytes, header_signature: str, secret: str) -> bool:
    digest = hmac.new(
        key=secret.encode("utf-8"),
        msg=raw_body,
        digestmod=hashlib.sha256,
    ).hexdigest()

    # Suponemos el formato "sha256=<hex>"
    expected = f"sha256={digest}"
    return hmac.compare_digest(expected, header_signature)

Puedes usarlo en FastAPI así:

from fastapi import Request, Header, HTTPException, status

WEBHOOK_SECRET = "replace-me"

@router.post("/github-like")
async def receive_github_like_webhook(
    request: Request,
    x_hub_signature_256: str | None = Header(default=None),
):
    raw_body = await request.body()

    if not x_hub_signature_256:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="missing signature",
        )

    if not verify_hmac_sha256(raw_body, x_hub_signature_256, WEBHOOK_SECRET):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="invalid signature",
        )

    payload = await request.json()
    return {"ok": True, "event_type": payload.get("action")}

Este patrón se aplica a muchos webhooks, pero el formato exacto del encabezado y si incluye o no marcas de tiempo varían según el proveedor, así que en producción debes seguir la especificación oficial del proveedor. GitHub usa X-Hub-Signature-256, y Stripe usa Stripe-Signature.


6. Para la verificación al estilo Stripe, mantente lo más cerca posible del flujo de la librería oficial

Stripe recomienda usar su librería oficial para la verificación de firmas de webhooks. También deja claro que los elementos necesarios son el encabezado Stripe-Signature, el secreto del endpoint y el cuerpo bruto sin modificar.

Así que, para una integración con Stripe, el flujo conceptualmente se parece a esto:

# Este es un ejemplo conceptual. En la práctica, sigue el procedimiento
# recomendado por la librería stripe.
@router.post("/stripe")
async def receive_stripe_webhook(
    request: Request,
    stripe_signature: str | None = Header(default=None, alias="Stripe-Signature"),
):
    raw_body = await request.body()

    if not stripe_signature:
        raise HTTPException(status_code=400, detail="missing Stripe-Signature")

    # Conceptualmente, aquí pasarías esto a algo como:
    # stripe.Webhook.construct_event(raw_body, stripe_signature, endpoint_secret)
    event = {"type": "payment_intent.succeeded"}  # marcador explicativo

    return {"received": True, "type": event["type"]}

El punto importante aquí es el orden: no inspecciones el tipo de evento hasta que la verificación de firma haya pasado.
Puede ser tentador ramificar antes según el tipo de evento, pero verificar si el remitente es legítimo debe ir primero.


7. Idempotencia: asegúrate de que el sistema no se rompa si el mismo evento llega dos veces

Los webhooks pueden entregarse más de una vez. Si el receptor hace timeout o la red es inestable, el emisor no puede estar seguro de que la entrega tuvo éxito, así que reintenta. Por eso necesitas idempotencia. En otras palabras, el sistema debe terminar en el mismo estado final aunque reciba dos veces el mismo evento. Las mejores prácticas de webhooks también colocan la idempotencia en el centro de un diseño robusto.

El método más práctico es almacenar el ID del evento o el ID de entrega asignado por el proveedor y comprobar si ya ha sido procesado. GitHub proporciona identificadores de entrega mediante encabezados y payloads, y Stripe también proporciona IDs de evento dentro de los objetos de evento.

7.1 Ejemplo de modelo para almacenar eventos procesados

from pydantic import BaseModel
from datetime import datetime

class ProcessedWebhookEvent(BaseModel):
    provider: str
    event_id: str
    received_at: datetime

7.2 Ejemplo de comprobación simple de duplicados

from fastapi import HTTPException, status

processed_event_ids: set[str] = set()

def ensure_not_processed(provider: str, event_id: str) -> None:
    key = f"{provider}:{event_id}"
    if key in processed_event_ids:
        raise HTTPException(
            status_code=status.HTTP_200_OK,
            detail="already processed",
        )
    processed_event_ids.add(key)

En producción, por supuesto, almacenarías esto en una base de datos o en Redis y no en memoria.
Aun así, el concepto de diseño es fácil de entender: almacena los IDs de evento bajo una restricción única y evita el procesamiento duplicado.


8. Prepárate para entregas fuera de orden y retrasadas: no confíes demasiado en la secuencia de eventos

Los webhooks no necesariamente llegan en orden cronológico.
Por eso, si codificas de forma rígida supuestos como “B siempre llegará después de A”, tu sistema se volverá frágil. Las mejores prácticas generales para webhooks recomiendan diseñar el receptor bajo la suposición de que la entrega fuera de orden y los reintentos son normales.

Una forma estable de pensar esto es:

  • Tratar el webhook como un disparador para refrescar o reconsiderar el estado
  • Si es necesario, volver a consultar la API del proveedor para obtener el estado más reciente y autoritativo
  • No determinar el estado completo únicamente a partir del evento recibido

Por ejemplo, en una integración de pagos, en lugar de decir “una vez que reciba un evento de pago exitoso, finalizaré inmediatamente el estado de facturación”, suele ser más robusto decir “después de recibir el evento, puedo volver a consultar el estado actual del pago si es necesario”. Stripe también posiciona los webhooks como un punto de entrada para el manejo asíncrono de eventos.


9. Devuelve el ACK rápidamente: empuja el trabajo pesado hacia el sistema posterior

Los emisores de webhooks suelen esperar una respuesta 2xx en poco tiempo. Si tu receptor realiza agregaciones pesadas o grandes actualizaciones de base de datos antes de responder, aumentan las probabilidades de timeout y de provocar reintentos. Por eso el enfoque estándar es hacer primero la verificación, la persistencia mínima y el ACK, y luego delegar el procesamiento posterior a una tarea en segundo plano o una cola. BackgroundTasks de FastAPI puede ejecutar trabajo ligero después de enviar la respuesta.

9.1 Ejemplo usando BackgroundTasks de FastAPI

from fastapi import BackgroundTasks, Request, Header

def process_webhook_event(provider: str, event_id: str, payload: dict) -> None:
    # En la práctica, esto actualizaría una BD o encolaría un trabajo
    print(provider, event_id, payload.get("type"))

@router.post("/generic-async")
async def receive_webhook_async(
    request: Request,
    background_tasks: BackgroundTasks,
    x_signature: str | None = Header(default=None),
):
    raw_body = await request.body()

    if not x_signature:
        raise HTTPException(status_code=400, detail="missing signature")

    # Realizar aquí la verificación de firma
    payload = await request.json()

    provider = "generic"
    event_id = payload.get("id")
    if not event_id:
        raise HTTPException(status_code=400, detail="missing event id")

    background_tasks.add_task(process_webhook_event, provider, event_id, payload)
    return {"received": True}

Sin embargo, BackgroundTasks es más adecuado para trabajo ligero posterior a la respuesta dentro del mismo proceso.
Para tareas pesadas o cualquier cosa que necesite reintentos, suele ser más estable delegar el trabajo a una cola como Celery. La documentación de FastAPI también presenta BackgroundTasks como un mecanismo para trabajo posterior a la respuesta.


10. Enrutamiento de eventos: evita que crezca una gran escalera de if

A medida que crece el número de tipos de evento de webhook, un solo endpoint puede acumular fácilmente una larga cadena de condiciones if event_type == ....
Un enfoque más limpio es recopilar las funciones manejadoras específicas del evento en un diccionario.

from collections.abc import Callable

def handle_payment_succeeded(payload: dict) -> None:
    ...

def handle_payment_failed(payload: dict) -> None:
    ...

HANDLERS: dict[str, Callable[[dict], None]] = {
    "payment.succeeded": handle_payment_succeeded,
    "payment.failed": handle_payment_failed,
}

def dispatch_event(event_type: str, payload: dict) -> None:
    handler = HANDLERS.get(event_type)
    if handler:
        handler(payload)

Si llamas a esto desde el procesamiento posterior del webhook, el diff al añadir eventos nuevos sigue siendo pequeño.
También ayuda separar archivos por proveedor, como stripe_handlers.py y github_handlers.py, para que las responsabilidades sigan siendo visibles.


11. Registros de auditoría y registros de eventos: trátalos por separado de los logs normales

Dado que los webhooks son eventos importantes originados externamente, resulta muy útil mantener logs fáciles de auditar separados de los logs normales de la aplicación.

Como mínimo, es útil registrar:

  • provider (Stripe, GitHub, etc.)
  • event_id
  • event_type
  • received_at
  • signature_verified
  • request_id
  • processing_status
  • error_message (en caso de fallo)

Con eventos externos como los de Stripe y GitHub, poder rastrear “recibido”, “verificado” y “procesado o retenido” facilita mucho la investigación de incidentes. GitHub también expone varios encabezados e identificadores de entrega que ayudan a rastrear las entregas.


12. ¿Qué debería devolver 2xx y qué debería devolver 4xx o 5xx?

En los sistemas de webhooks, la respuesta del receptor afecta al comportamiento de reintento.
Una clasificación práctica es:

  • Firma inválida, cuerpo mal formado, encabezados obligatorios ausentes
    → 4xx (la entrega no cumple tus condiciones de aceptación)
  • Fallos internos temporales, caídas de base de datos
    → 5xx (diseñado bajo la suposición de que pueden ocurrir reintentos)
  • Eventos duplicados ya procesados
    → a menudo es seguro tratarlos como 2xx

La elección final depende de la política de reintentos de cada proveedor, por lo que el comportamiento en producción debe ajustarse a su especificación. Aun así, el principio de que los duplicados pueden tratarse como exitosos, pero las firmas inválidas nunca deben tratarse como exitosas es útil en muchos casos. Tanto GitHub como Stripe enfatizan primero confirmar la legitimidad de la entrega.


13. Normalmente es mejor separar las URLs de webhook por proveedor

Es posible unificar los webhooks de múltiples proveedores en un solo receptor, pero en la práctica suele ser más seguro y más fácil de operar si separas la URL por proveedor.

Por ejemplo:

  • /webhooks/stripe
  • /webhooks/github
  • /webhooks/internal

Esto te da varias ventajas:

  • Los secretos se gestionan por separado
  • Las diferencias en los esquemas de firma son más fáciles de absorber
  • Los logs y el monitoreo son más fáciles de separar
  • La investigación de incidentes se vuelve más rápida

Dado que GitHub y Stripe usan nombres de encabezado y formatos de firma distintos, separarlos desde el principio es el diseño más natural.


14. Estrategia de pruebas: cinco pruebas que merece la pena proteger primero

Los receptores de webhooks pueden parecer simples, pero son lo suficientemente frágiles como para que las pruebas importen mucho.
Incluso solo las siguientes cinco pruebas añaden una confianza significativa:

  1. Una solicitud con firma correcta es aceptada
  2. Una solicitud con firma inválida es rechazada
  3. Enviar el mismo ID de evento dos veces no provoca procesamiento duplicado
  4. Un fallo en el procesamiento posterior pesado no rompe la responsabilidad principal del receptor
  5. La ausencia de encabezados obligatorios o de un ID de evento obligatorio provoca rechazo

Puedes probar esto con el estilo normal de pruebas de API de FastAPI usando TestClient, prestando atención a los encabezados de firma y al cuerpo bruto. Si construyes tus casos de prueba alrededor del patrón del que dependen tanto GitHub como Stripe —encabezado de firma + cuerpo bruto + secreto—, tus pruebas serán realmente útiles en operación real.


15. Hoja de ruta según el lector

Para desarrolladores individuales y personas que están aprendiendo

  1. Empieza separando las URLs de webhook por proveedor
  2. Construye la estructura para recibir el cuerpo bruto con Request.body()
  3. Añade verificación de firma basada en secreto
  4. Almacena los IDs de evento y evita el procesamiento duplicado
  5. Delegar el procesamiento pesado a BackgroundTasks o a una cola de trabajos

Para ingenieros backend en equipos pequeños

  1. Compartir con el equipo la separación de responsabilidades de “recibir, verificar, ACK, procesamiento posterior”
  2. Consolidar la verificación de firmas específica por proveedor en funciones compartidas
  3. Poner en marcha una tabla de eventos y registros de auditoría
  4. Añadir persistencia para claves de idempotencia como los IDs de evento
  5. Proteger con pruebas los duplicados, las firmas inválidas y casos cercanos a entregas fuera de orden

Para equipos SaaS y startups

  1. Rediseñar la recepción del webhook como un punto de entrada de eventos de dominio
  2. Separar registros de auditoría, tabla de eventos y cola de trabajos
  3. Definir políticas de reintento y fallo por proveedor
  4. Monitorizar métricas como número de eventos recibidos, fallos de verificación de firma, número de duplicados y número de fallos de procesamiento
  5. Conectar los webhooks de pagos y cambios de contrato con el estado de facturación y los registros de auditoría como parte de un diseño de sistema completo

Referencias


Conclusión

  • Una API receptora de webhooks es un punto de entrada que debe construirse con más cuidado que una API POST normal. Una vez que se incluyen verificación de firma, idempotencia, manejo de reintentos y auditabilidad, resulta mucho más difícil que falle en operación real.
  • En FastAPI, un patrón base especialmente compatible es recuperar el cuerpo bruto desde Request, verificar la firma, devolver un ACK rápidamente y pasar el procesamiento posterior a tareas en segundo plano o a una cola de trabajos.
  • Servicios importantes como Stripe y GitHub también parten de la verificación de firmas basada en secreto. Lo mejor es seguir primero sus especificaciones oficiales y absorber las diferencias específicas de cada proveedor en el diseño de tu receptor y en funciones auxiliares compartidas.
  • No necesitas construir una plataforma de eventos perfecta desde el principio, pero simplemente separar recibir, verificar, registrar, ACK y procesamiento posterior ya hace que el sistema sea mucho más resistente a la complejidad futura.

Como temas naturales a continuación, este artículo conecta muy bien con asuntos como “Diseñar clientes de API externas en FastAPI (reintentos, timeouts, circuit breakers)” y “Cómo conectar el diseño de colas de trabajos en FastAPI con el procesamiento posterior de webhooks”.

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)