Módulo 3 · Diseño de APIs

Lección 17 — Webhooks y APIs de terceros

Reintentos, timeouts y firmas: el webhook de la pasarela de pago que confirma tu entrada.

Publicada
En esta lección
  1. Objetivos
  2. 1. El webhook entrante: confianza cero
  3. 2. Responder rápido, procesar después
  4. 3. Llamar APIs de terceros: tú también eres cliente
  5. 4. Tu webhook saliente (cuando otros te consumen)
  6. 5. Webhooks entrantes vs salientes: la asimetría que manda
  7. 6. Seguridad del endpoint entrante: checklist de la fortaleza

Stack: DRF + Celery (adelanto) · Proyecto: TicketFlow Estado: Publicada — cierre del módulo de APIs Prerrequisito: Lección 16 — gRPC/GraphQL/WS/SSE


Objetivos

  1. Recibir el webhook de la pasarela con seguridad: verificación de firma, replay y orden.
  2. Responder 2xx rápido y procesar después (el patrón que evita reintentos tormentosos).
  3. Diseñar tu webhook saliente para otros (idempotencia, reintentos, firmas) — la simetría del oficio.

1. El webhook entrante: confianza cero

La pasarela (Stripe/PayPal/simulada) llama a tu URL pública cuando el pago cambia de estado. Tu handler no puede creer el cuerpo: cualquiera puede POSTear {"status": "SUCCEEDED"}. Defensas, en orden:

  1. Firma HMAC: el proveedor firma el payload con un secreto compartido; tú recalculas con el raw body y comparas (constante-time). Sin coincidencia → 401/400 y log.
  2. Anti-replay: el payload lleva timestamp; si tiene más de 5 minutos, se rechaza (un cuerpo robado no vale para siempre). Stripe firma timestamp.payload y el timestamp es parte del HMAC.
  3. Idempotencia por event_id: el proveedor reintenta (at-least-once, la 14): buscar antes de crear; duplicado → 200 silencioso.
  4. Orden no garantizado: payment.succeeded puede llegar antes que payment.pending (reintentos). Tu procesamiento debe ser tolerante: el estado del pago lo gobierna la máquina de estados (00b), no el orden de llegada.
python
@api_view(["POST"])
@authentication_classes([])  # el webhook no usa tu auth: usa su firma
@permission_classes([AllowAny])
def gateway_webhook(request):
    signature = request.headers.get("X-Signature", "")
    if not verify_hmac(request.body, signature, settings.GATEWAY_SECRET):
        return Response(status=401)
    payload = json.loads(request.body)
    if abs(time.time() - payload["timestamp"]) > 300:
        return Response(status=400)
    process_gateway_event.delay(payload)   # enqueue: la 29
    return Response(status=200)            # ACK inmediato

2. Responder rápido, procesar después

¿Por qué delay() y no procesar ahí? Porque el proveedor reintenta con timeout (típicamente 5-30s) y con backoff exponencial si fallas. Si tu handler hace: verificar pago en BD, enviar email, generar entradas PDF... y tarda 40s, el proveedor te marca fallo y reintenta mientras tu primer procesamiento sigue vivo → doble procesamiento + tormenta. El patrón: ACK 2xx al recibir (validado + encolado), procesamiento en worker (Celery, Lección 29), idempotencia del worker por event_id.

Si aún no tienes Celery: procesar síncrono pero acotado (solo actualizar estado + crear entrada en BD, <1s) es aceptable para empezar; la cola es la forma adulta.

3. Llamar APIs de terceros: tú también eres cliente

Cuando TÚ llamas a la pasarela para crear el pago (antes del webhook):

  • Timeout explícito (connect+read, p. ej. 3s/10s): el default de la librería es 60s+ = tu worker colgado.
  • Reintentos con backoff solo para idempotentes: GET sí; POST de pago con la Idempotency-Key del proveedor (siempre que lo ofrezca) — así el reintento no cobra dos veces (14).
  • Circuit breaker (adelanto 54): si la pasarela lleva 30 fallos, corta y falla rápido en vez de acumular timeouts.
  • Nunca secretos en logs (la 05) y credenciales por entorno (27).

4. Tu webhook saliente (cuando otros te consumen)

TicketFlow avisará a los organizadores ("vendiste 50 entradas"): misma disciplina al revés — firma HMAC del cuerpo + timestamp, reintentos con backoff (5 intentos, 1m/5m/30m/2h/6h), idempotencia con event_id, y un panel de entregas fallidas (endpoint de salud de webhooks salientes: última entrega, código, reintento programado). Un webhook saliente sin reintentos es una promesa que no cumples.

5. Webhooks entrantes vs salientes: la asimetría que manda

Dos casas viven en TicketFlow y no se gobiernan igual:

Entrantes (la pasarela nos llama)Salientes (nosotros llamamos)
Quién fija las reglasEl tercero (su formato, su firma)Nosotros (nuestro formato, nuestra firma)
VerificaciónVerificar SU HMAC (raw body + ventana)Firmar NUESTRO HMAC y publicar la clave
Fiabilidad exigibleSus reintentos (estándar: 24-72 h)Nuestros reintentos (backoff 1m→6h, DLQ)
El fracaso típicoNo verificar (el 402 que forjó pagos)No deduplicar en el receptor ajeno (el doble evento)
ReconciliaciónEl GET del intent ante el UNKNOWN (32)El log de intentos + el reporte de entrega

El detalle que conecta con el 25/30: los salientes NO se envían directamente del servicio — se escriben al outbox y un poller los publica (el mismo mecanismo de los eventos de dominio): la garantía transaccional ("solo notificamos lo que de verdad pasó") es la misma para ambos usos. Y el receptor ajeno de nuestros webhooks merece la misma generosidad que exigimos: reintentos con backoff, firma verificable y documentación de la ventana — el webhook mal diseñado es el que obliga al ajeno a poller.

6. Seguridad del endpoint entrante: checklist de la fortaleza

El endpoint que recibe webhooks es público por definición (la pasarela debe alcanzarlo). El blindaje completo: (1) verificación HMAC SIEMPRE (raw body, clave por tercero, rotación dual de la 27); (2) ventana anti-replay (5 min) + guard de timestamp fuera de ventana; (3) dedup por event_id del tercero (la 30: at-least-once ES lo normal); (4) respuesta 200 inmediata + trabajo en cola (el ACK no es el trabajo: es el recibido); (5) rate limit por origen (23: la pasarela no necesita 1000 rps; un atacante sí); (6) el secret jamás en el log (23: el header Authorization con el HMAC se filtra en logs de proxy: filtrar la 45); (7) monitoreo de firma inválida (46: picos de firma inválida = alguien intentando forjar). Con las siete, el webhook entrante de TicketFlow es tan auditable como el resto del sistema.


  1. ¿Por qué se verifica la firma sobre el raw body y no sobre request.data ya parseado?
  2. ¿Qué doble función cumple el timestamp firmado (anti-replay) y por qué 5 minutos?
  3. ¿Por qué ACK antes de procesar y qué pasa exactamente si procesas lento sin cola?
  4. El webhook llegó fuera de orden (succeeded antes que pending): ¿qué componente de tu modelo absorbe el desorden?
  5. Tu webhook saliente falló 5 veces para un organizador. ¿Qué sigue? ¿Y si la URL del organizador ahora devuelve 410?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.