Hello, team! I’ve been fighting against this Stripe webhook for days now. I’m praying someone here can help.
I have a lot going on with this site, so I’ll try to keep the parts I believe are most relevant, but please feel free to ask me for any additional information you need:
Issue:
Stripe is showing that my webhook endpoint is not being reached: it has a 405 “Method not allowed” status code:
My setup:
I’m deployed via AWS Fargate with a load balancer. I have a rule set up in my load balancer to allow traffic to come in from my webhook address.
I have significant logging in my views, but NONE of the logs show up from my stripe_webhook view. That means it’s not even being reached at all. And, since it’s not being reached, my tagging isn’t being added to Mailchimp the way it should.
However, the site works fine: I’m able to purchase a report, download it, etc. It’s just the webhook that’s the issue.
My last effort was trying to bypass Django’s middleware (I don’t have anything custom there) because I thought it may be blocking it somehow.
No luck.
I’ll attach my relevant functions from payments/views, and my urls too.
Can someone help me out with this? I’m MORE than willing to pay you!!
@csrf_exempt
@require_http_methods(["POST"])
def create_checkout_session(request):
"""
Creates a new Stripe checkout session and initiates report generation.
This function handles the entire checkout flow:
1. Validates the property exists
2. Creates a report record
3. Initiates report generation
4. Creates and configures Stripe checkout session
The report generation starts immediately for faster user experience,
with proper cleanup mechanisms in case of cancellation.
"""
logger.info(f"Starting checkout session creation for user: {request.user.username}")
try:
# Transaction to ensure database consistency
with transaction.atomic():
# Validate property
property_id = request.session.get('property_id')
if not property_id:
logger.error("No property_id found in session")
return JsonResponse({
'error': 'No property found in session. Please start over.'
}, status=400)
try:
property_instance = Property.objects.get(id=property_id)
logger.info(f"Found property: {property_id} for user: {request.user.username}")
except Property.DoesNotExist:
logger.error(f"Property not found: {property_id}")
return JsonResponse({
'error': 'Property not found. Please try again.'
}, status=404)
# Create report record
try:
report = UserReport.objects.create(
user=request.user,
property=property_instance,
status='pending_payment',
created_at=timezone.now()
)
logger.info(f"Created report record: {report.id}")
except Exception as e:
logger.error(f"Failed to create report record: {str(e)}")
return JsonResponse({
'error': 'Failed to create report record'
}, status=500)
# Start PDF generation
try:
task = generate_report_task.delay(report.id)
report.celery_task_id = task.id
report.save(update_fields=['celery_task_id'])
logger.info(f"Started report generation task: {task.id} for report: {report.id}")
except Exception as e:
logger.error(f"Failed to start report generation: {str(e)}")
# Cleanup on failure
if hasattr(report, 'celery_task_id'):
AsyncResult(report.celery_task_id).revoke(terminate=True)
if report.report_file:
try:
storage = get_storage_class(settings.PRIVATE_FILE_STORAGE)()
storage.delete(report.report_file.name)
except Exception as file_e:
logger.error(f"Failed to delete report file: {str(file_e)}")
report.delete()
return JsonResponse({
'error': 'Failed to start report generation'
}, status=500)
# Create Stripe checkout session
try:
# Ensure HTTPS for production
domain_url = request.build_absolute_uri('/').rstrip('/')
if not settings.DEBUG and not domain_url.startswith('https'):
domain_url = domain_url.replace('http://', 'https://')
checkout_session = stripe.checkout.Session.create(
success_url=domain_url + reverse('reports:dashboard') + f'?session_id={report.id}',
cancel_url=domain_url + reverse('payments:cancelled') + f'?report_id={report.id}',
payment_method_types=['card'],
mode='payment',
metadata={
'report_id': str(report.id),
'user_id': str(request.user.id),
'property_id': str(property_id)
},
line_items=[{
'price_data': {
'currency': 'usd',
'product_data': {
'name': 'Property Tax Protest Report',
'description': f'Report for {property_instance.address}'
},
'unit_amount': 2700,
},
'quantity': 1,
}],
client_reference_id=str(request.user.id)
)
# Save Stripe session ID
report.stripe_session_id = checkout_session.id
report.save(update_fields=['stripe_session_id'])
logger.info(f"Created Stripe session: {checkout_session.id} for report: {report.id}")
return JsonResponse({'sessionId': checkout_session.id})
except stripe.error.StripeError as e:
logger.error(f"Stripe error creating session: {str(e)}")
# Cleanup on Stripe failure
if hasattr(report, 'celery_task_id'):
AsyncResult(report.celery_task_id).revoke(terminate=True)
report.delete()
return JsonResponse({'error': str(e)}, status=400)
except Exception as e:
logger.error(f"Unexpected error in checkout process: {str(e)}", exc_info=True)
return JsonResponse({
'error': 'An unexpected error occurred. Please try again.'
}, status=500)
def handle_successful_payment(session):
"""Log successful payment confirmation."""
report_id = session.get('metadata', {}).get('report_id')
if report_id:
logger.info(f"Payment confirmed for report {report_id}")
else:
logger.error("Missing report ID in session metadata")
@require_http_methods(["POST", "GET"])
@csrf_exempt
def stripe_webhook(request):
"""
Webhook handler for Stripe events with Mailchimp integration.
This function validates incoming Stripe events, processes completed
checkout sessions, and adds purchase tags to Mailchimp for successful
payments.
"""
logger.info("=== WEBHOOK REQUEST RECEIVED ===")
logger.info(f"Request method: {request.method}")
logger.info(f"Content type: {request.content_type}")
logger.debug(f"Headers: {dict(request.headers)}")
if request.method != 'POST':
# Return 200 OK for GET requests (for health checks)
return HttpResponse("Webhook endpoint operational", status=200)
# Validate Stripe signature
webhook_secret = settings.STRIPE_WEBHOOK_SECRET
payload = request.body
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
if not sig_header:
logger.error("No Stripe signature header found - possible unauthorized request")
return HttpResponse(status=400)
logger.info(f"Signature header present: {sig_header[:10]}...")
try:
# Log raw payload (redacted for security)
payload_text = payload.decode('utf-8')
try:
payload_json = json.loads(payload_text)
event_type = payload_json.get('type', 'unknown')
event_id = payload_json.get('id', 'unknown')
logger.info(f"Raw event type: {event_type}, ID: {event_id}")
except json.JSONDecodeError:
logger.warning("Could not parse JSON payload for logging")
# Verify the event is from Stripe
event = stripe.Webhook.construct_event(
payload, sig_header, webhook_secret
)
logger.info(f"Successfully validated Stripe webhook event: {event['type']} with ID: {event['id']}")
# Process event based on type
if event['type'] == 'checkout.session.completed':
logger.info("Processing checkout.session.completed event")
process_checkout_completed(event)
elif event['type'] == 'payment_intent.succeeded':
logger.info(f"Payment intent succeeded: {event['data']['object'].get('id')}")
elif event['type'] == 'charge.succeeded':
logger.info(f"Charge succeeded: {event['data']['object'].get('id')}")
else:
logger.info(f"Received unhandled event type: {event['type']}")
# Return success response
logger.info("Webhook processed successfully")
return HttpResponse(status=200)
except ValueError as e:
logger.error(f"Invalid payload received from Stripe: {str(e)}")
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
logger.error(f"Invalid Stripe signature: {str(e)}")
return HttpResponse(status=400)
except Exception as e:
logger.error(f"Unexpected error in webhook: {str(e)}", exc_info=True)
return HttpResponse(status=500)
def process_checkout_completed(event):
"""
Process a completed checkout session and update Mailchimp tags.
Args:
event: The Stripe event containing checkout session data
"""
session = event['data']['object']
logger.info(f"Processing completed checkout session: {session.get('id')}")
# Extract metadata
metadata = session.get('metadata', {})
logger.info(f"Session metadata: {metadata}")
# Extract report_id from metadata
report_id = metadata.get('report_id')
if not report_id:
logger.error("Report ID missing from session metadata")
return
logger.info(f"Found report_id in metadata: {report_id}")
try:
with transaction.atomic():
# Get the report
report = UserReport.objects.select_for_update().get(id=report_id)
logger.info(f"Found report {report_id} for user {report.user.email}")
# Update report status
report.status = 'paid'
report.payment_confirmed_at = datetime.now()
report.save(update_fields=['status', 'payment_confirmed_at'])
logger.info(f"Successfully updated report {report_id} status to 'paid'")
# Get user email
email = report.user.email
logger.info(f"Processing Mailchimp tags for email: {email}")
# Add purchase tags to Mailchimp
try:
# Try to add tags using the imported function
result = add_purchase_tags(email)
if result:
logger.info(f"Successfully added purchase tags for {email}")
else:
logger.warning(f"Failed to add purchase tags for {email}")
report.notes = f"{report.notes or ''} Failed to add Mailchimp purchase tags"
report.save(update_fields=['notes'])
except Exception as e:
logger.error(f"Error adding Mailchimp purchase tags: {str(e)}", exc_info=True)
report.notes = f"{report.notes or ''} Tag error: {str(e)}"
report.save(update_fields=['notes'])
except UserReport.DoesNotExist:
logger.error(f"Report {report_id} not found for completed payment")
except Exception as e:
logger.error(f"Error processing completed session: {str(e)}", exc_info=True)
# payments/urls.py
from django.urls import path
from .views import stripe_config, create_checkout_session, stripe_webhook, HomePageView, SuccessView, CancelledView
app_name = 'payments'
urlpatterns = [
path('config/', stripe_config, name='config'),
path('create-checkout-session/', create_checkout_session, name='create_checkout_session'),
path('webhook/', stripe_webhook, name='webhook'),
path('success/', SuccessView.as_view(), name='success'),
path('cancelled/', CancelledView.as_view(), name='cancelled'),
path('', HomePageView.as_view(), name='home'),
]