Endpoint POST /api/billing/webhook/stripe — Receptor de eventos de Stripe
Resumen
Endpoint público (sin autenticación de usuario) que recibe eventos del webhook de Stripe. Verifica la firma HMAC del header Stripe-Signature contra la clave STRIPE_WEBHOOK_SECRET y procesa o almacena cada evento en el ledger global StripeEvent.
Ubicación: billing/api.py:stripe_webhook(request) → registrado en config/urls.py como /api/billing/webhook/stripe
Firma y autenticación
@router.post("/webhook/stripe", auth=None)
def stripe_webhook(request):
- Auth:
auth=None— sin sesión de usuario (Stripe no tiene token) - Seguridad: Verificación HMAC-SHA256 del header
Stripe-Signature: t=<epoch>,v1=<mac>usandostripe.Webhook.construct_event() - Fail-closed: Si
STRIPE_WEBHOOK_SECRETno está configurado (dev, test, PROD antes de PR-4), responde 503 Service Unavailable y no procesa nada
Flujo de procesamiento
- Extrae firma del header
Stripe-Signature - Verifica HMAC → 400 Bad Request si no coincide o es vieja (tolerancia: 300 s de replay)
- Parsea payload JSON (body crudo)
- Get-or-create
StripeEventen la tabla ledger (clave:event_idúnico) - Si es reentrega (evento ya
processedoskipped) → responde 200 +{"duplicate": True}sin re-procesar - Si es nuevo → llama
apply_event(payload)para mapear estado de Stripe a Organization - Guarda resultado (status, org_id, timestamp) y responde 200
- En caso de error → status=
error, responde 500 para que Stripe reintente
Comportamientos notables
- Idempotencia: El evento se procesa máximo una vez (el
event_ides clave única) - Reintento de Stripe: El código 500 indica a Stripe que lo reintente; 200 (cualquier status) detiene los reintentos
- Sin RLS: La tabla
StripeEventes GLOBAL, sin FK a Organization, porque los eventos llegan sin tenant autenticado - No toca decisiones de producto: PR-2 solo actualiza
Organization.stripe_customer_id,subscription_status,current_period_end. No tocais_activeniplan(PR-3+)
Configuración (settings)
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET") or ""
# Vacío en dev/test; en PROD solo se activa en PR-4 tras ir-live en test mode
Eventos manejados
Mapeados en billing/services.py:apply_event():
checkout.session.completed→ liga customer a orgcustomer.subscription.created→ actualiza status + period_endcustomer.subscription.updated→ actualiza status + period_end (fallback a items para API Basil 2025-03+)customer.subscription.deleted→ marca comocanceledinvoice.payment_failed→ marca comopast_due
Otros tipos → skipped (200 sin error)
Tests
tests/billing/test_stripe_webhook.py (12 tests):
- Seguridad: firma inválida/ausente/vieja, sin secret
- Procesamiento: checkout, subscription events, invoice failed
- Idempotencia: reentregas
- Edge cases: customer no resoluble, status de Stripe desconocido, tipos de evento no manejados
Véase también
- [[entity—billing—model—stripe-event]]
- [[entity—billing—service—event-applier]]
- [[entity—billing—task—stripe-reconciliation]]
- [[feature—billing—stripe-pr2-webhooks]]
- [[entity—core—model—organization]]