# app/api/v1/shared/webhooks.py """ External webhook endpoints. Handles webhooks from: - Stripe (payments and subscriptions) """ import logging from fastapi import APIRouter, Header, Request from sqlalchemy.orm import Session from app.core.database import get_db from app.exceptions import InvalidWebhookSignatureException, WebhookMissingSignatureException from app.services.stripe_service import stripe_service from app.handlers.stripe_webhook import stripe_webhook_handler router = APIRouter(prefix="/webhooks") logger = logging.getLogger(__name__) @router.post("/stripe") # public - Stripe webhooks use signature verification async def stripe_webhook( request: Request, stripe_signature: str = Header(None, alias="Stripe-Signature"), ): """ Handle Stripe webhook events. Stripe sends events for: - Subscription lifecycle (created, updated, deleted) - Invoice and payment events - Checkout session completion The endpoint verifies the webhook signature and processes events idempotently. """ if not stripe_signature: logger.warning("Stripe webhook received without signature") raise WebhookMissingSignatureException() # Get raw body for signature verification payload = await request.body() try: # Verify and construct event event = stripe_service.construct_event(payload, stripe_signature) except ValueError as e: logger.warning(f"Invalid Stripe webhook: {e}") raise InvalidWebhookSignatureException(str(e)) # Process the event db = next(get_db()) try: result = stripe_webhook_handler.handle_event(db, event) return {"received": True, **result} except Exception as e: logger.error(f"Error processing Stripe webhook: {e}") # Return 200 to prevent Stripe retries for processing errors # The event is marked as failed and can be retried manually return {"received": True, "error": str(e)} finally: db.close()