CreaRack-SL

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> usando stripe.Webhook.construct_event()
  • Fail-closed: Si STRIPE_WEBHOOK_SECRET no está configurado (dev, test, PROD antes de PR-4), responde 503 Service Unavailable y no procesa nada

Flujo de procesamiento

  1. Extrae firma del header Stripe-Signature
  2. Verifica HMAC → 400 Bad Request si no coincide o es vieja (tolerancia: 300 s de replay)
  3. Parsea payload JSON (body crudo)
  4. Get-or-create StripeEvent en la tabla ledger (clave: event_id único)
  5. Si es reentrega (evento ya processed o skipped) → responde 200 + {"duplicate": True} sin re-procesar
  6. Si es nuevo → llama apply_event(payload) para mapear estado de Stripe a Organization
  7. Guarda resultado (status, org_id, timestamp) y responde 200
  8. 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_id es 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 StripeEvent es 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 toca is_active ni plan (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 org
  • customer.subscription.created → actualiza status + period_end
  • customer.subscription.updated → actualiza status + period_end (fallback a items para API Basil 2025-03+)
  • customer.subscription.deleted → marca como canceled
  • invoice.payment_failed → marca como past_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]]