stripe webhook not working in Django view

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'),
]

If you remove all of your stripe related code, and POST to the endpoint locally localhost:8000/payments/webhook/, does that also result in a 405?

If it’s a Django problem, and I had to take a random guess, you might have another url that is matching that path in your urlpatterns (in another app, for example)

If it’s not a Django problem, but you’re not seeing the logs you expect, what do you see in your load balancer logs?

Hey massover! Thanks for jumping in! It works any other way except for in Production. It worked in Staging too (VERY close to production setup). However, that code has since been merged with production.

I’ll attach all of my urls here. Let me know if you see something:

django_project/urls

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static 
from apps.payments.views import stripe_config
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET
import logging

logger = logging.getLogger(__name__)

@login_required
def test_view(request):
    return HttpResponse(f"Logged in as: {request.user.username}, Superuser: {request.user.is_superuser}, Session key: {request.session.session_key}")

@csrf_exempt
@require_GET
def health_check(request):
    return HttpResponse("OK", status=200)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('apps.accounts.urls', namespace='accounts')),
    path('accounts/', include('django.contrib.auth.urls')),
    path('payments/', include('apps.payments.urls', namespace='payments')),
    path('reports/', include('apps.reports.urls', namespace='reports')),
    path('config/', stripe_config, name='stripe_config'),
    path('', include('apps.pages.urls', namespace='pages')),
    path('ckeditor5/', include('django_ckeditor_5.urls')),
    path('health/', health_check, name='health_check'),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# 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'),
]

#apps/accounts/urls

from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    path('signup/', views.SignUpView.as_view(), name='signup'),
    path('profile/', views.profile_view, name='profile'),
    path('update-personal-info/', views.update_personal_info, name='update_personal_info'),
    path('update-address/', views.update_address, name='update_address'),
    path('verify-declaration/', views.verify_declaration, name='verify_declaration'),
    path('add-address/', views.add_address, name='add_address'),
    path('update-additional-address/<int:address_id>/', views.update_additional_address, name='update_additional_address'),
    path('delete-additional-address/<int:address_id>/', views.delete_additional_address, name='delete_additional_address'),
]
# pages/urls.py

from django.urls import path
from .views import (
    HomePageView, 
    FAQView, 
    download_guide, 
    track_guide_download,
    ContactView, 
    contact_success,
    DemoView
)

app_name = 'pages'

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
    path('download-guide/', download_guide, name='download_guide'),
    path('track-guide-download/', track_guide_download, name='track_guide_download'),
    path('faq/', FAQView.as_view(), name='faq'),
    path('faq/<slug:slug>/', FAQView.as_view(), name='faq_detail'),
    path('contact/', ContactView.as_view(), name='contact'),
    path('contact/success/', contact_success, name='contact_success'),
    path('demo/', DemoView.as_view(), name='demo'),
]
from django.urls import path 
from .views import (
    check_status,
    evaluation_form,
    evaluation_submission,
    purchase_report,
    update_protest_status,
    delete_report,
    dashboard,
    agree_to_terms,
    update_value,
    GeneratePDFView
)

app_name = 'reports'
urlpatterns = [
    path('evaluation-form/', evaluation_form, name='evaluation_form'),
    path('evaluation-submission/', evaluation_submission, name='evaluation_submission'),
    path('purchase-report/', purchase_report, name='purchase_report'),
    path('generate-pdf/', GeneratePDFView.as_view(), name='generate_pdf'),
    path('download-report/<int:report_id>/', GeneratePDFView.as_view(), name='download_report'),
    path('delete-report/<int:report_id>/', delete_report, name='delete_report'),
    path('update-protest-status/', update_protest_status, name='update_protest_status'),
    path('update-value/', update_value, name='update_value'),
    path('dashboard/', dashboard, name='dashboard'),
    path('agree-to-terms/', agree_to_terms, name='agree_to_terms'),
    path('check-status/<int:report_id>/', check_status, name='check_status'),
]

If you can’t reproduce the 405 locally, then it’s likely not a code problem. I don’t see anything wrong with the urls.

Check that all of your infrastructure is set up as you expect. You mention:

I have a rule set up in my load balancer to allow traffic to come in from my webhook address

If it works in staging, check that your configuration in staging really “matches” what you expect in production. You might have a typo in your production load balancer configuration (eg. missing a trailing slash) or even in the external stripe configuration.

When I test in Postman I get a 400 Bad Request. From what I understand, that’s expected because I’m using dummy data in Postman. But that means it’s at least reaching my Django application.

And after that test, I see from my logs that I’m getting an invalid Stripe signature, but I don’t know why:

{
    "timestamp": "2025-02-27 13:39:17",
    "level": "ERROR",
    "logger": "apps.payments.views",
    "message": "Invalid Stripe signature: No signatures found matching the expected signature for payload",
    "path": "/app/apps/payments/views.py",
    "lineno": 283
}

Have you checked your production server logs?
Normally this “method_not_allowed” thing happens when there’s a redirect by django, most of the times that is caused by a malformed URL, for example, missing an ending slash at the end of the URL.

I don’t see anything in the Stripe screenshot that specifies what method it was using for the request that failed. Is that shown somewhere not visible on the screenshot? I’d assume that it sent something other than a GET or POST, given those are the only two methods allowed by the stripe_webook() view.

My CloudWatch logs show this after running the test in Postman:

{
    "timestamp": "2025-02-27 13:39:17",
    "level": "ERROR",
    "logger": "apps.payments.views",
    "message": "Invalid Stripe signature: No signatures found matching the expected signature for payload",
    "path": "/app/apps/payments/views.py",
    "lineno": 283
}

@philgyford: All Stripe shows is 405 (Method Not Allowed):

And it has this as the Request (from Stripe dashboard):

Request
{
  "id": "evt_3QwnfOP5F4Iik8YN05qIir1n",
  "object": "event",
  "api_version": "2024-04-10",
  "created": 1740588064,
  "data": {
    "object": {
      "id": "ch_3QwnfOP5F4Iik8YN0lGd6VjB",
      "object": "charge",
      "amount": 2700,
      "amount_captured": 2700,
      "amount_refunded": 0,
      "application": null,
      "application_fee": null,
      "application_fee_amount": null,
      "balance_transaction": "txn_3QwnfOP5F4Iik8YN0EfI7kzj",
      "billing_details": {
        "address": {
          "city": "Peyton",
          "country": "US",
          "line1": "8720 Palomino Ridge Vw.",
          "line2": null,
          "postal_code": "80831",
          "state": "CO"
        },
        "email": "joshhayles07@gmail.com",
        "name": "Josh Hayles",
        "phone": null
      },
      "calculated_statement_descriptor": "TAXCORRECTOR.NET",
      "captured": true,
      "created": 1740588063,
      "currency": "usd",
      "customer": null,
      "description": null,
      "destination": null,
      "dispute": null,
      "disputed": false,
      "failure_balance_transaction": null,
      "failure_code": null,
      "failure_message": null,
      "fraud_details": {
      },
      "invoice": null,
      "livemode": true,
      "metadata": {
      },
      "on_behalf_of": null,
      "order": null,
      "outcome": {
        "advice_code": null,
        "network_advice_code": null,
        "network_decline_code": null,
        "network_status": "approved_by_network",
        "reason": null,
        "risk_level": "normal",
        "seller_message": "Payment complete.",
        "type": "authorized"
      },
      "paid": true,
      "payment_intent": "pi_3QwnfOP5F4Iik8YN0Ebjp1Mv",
      "payment_method": "pm_1QwnfMP5F4Iik8YNs9VOfMWm",
      "payment_method_details": {
        "card": {
          "amount_authorized": 2700,
          "authorization_code": "03111G",
          "brand": "visa",
          "checks": {
            "address_line1_check": "pass",
            "address_postal_code_check": "pass",
            "cvc_check": null
          },
          "country": "US",
          "exp_month": 11,
          "exp_year": 2028,
          "extended_authorization": {
            "status": "disabled"
          },
          "fingerprint": "MZoJe4TI3Qugfd4M",
          "funding": "credit",
          "incremental_authorization": {
            "status": "unavailable"
          },
          "installments": null,
          "last4": "6450",
          "mandate": null,
          "multicapture": {
            "status": "unavailable"
          },
          "network": "visa",
          "network_token": {
            "used": false
          },
          "network_transaction_id": "465057600636795",
          "overcapture": {
            "maximum_amount_capturable": 2700,
            "status": "unavailable"
          },
          "regulated_status": "unregulated",
          "three_d_secure": null,
          "wallet": {
            "dynamic_last4": null,
            "link": {
            },
            "type": "link"
          }
        },
        "type": "card"
      },
      "radar_options": {
      },
      "receipt_email": null,
      "receipt_number": null,
      "receipt_url": "https://pay.stripe.com/receipts/payment/CAcQARoXChVhY2N0XzFQUmkzelA1RjRJaWs4WU4ooIj9vQYyBoo3yCFv7TosFqCHR5IC1uQCgWkP1yk7y0EzpQyjP2C-ROKGFFyr7bstgN1D6UJQA4NdenI",
      "refunded": false,
      "review": null,
      "shipping": null,
      "source": null,
      "source_transfer": null,
      "statement_descriptor": null,
      "statement_descriptor_suffix": null,
      "status": "succeeded",
      "transfer_data": null,
      "transfer_group": null
    }
  },
  "livemode": true,
  "pending_webhooks": 1,
  "request": {
    "id": "req_Hak1U7qyYNed1C",
    "idempotency_key": "17094104-622d-4a64-8ce0-638ca40d6461"
  },
  "type": "charge.succeeded"
}

And this is my stripe_webhook:

@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)

[Bearing in mind I haven’t used Stripe webhooks for a few years…]

Looks like Stripe only sends POST requests to webhooks so I guess it’s not sending an unexpected method. Which puzzles me because that should be what a 405 response means.

You’re doing a lot of logging in the stripe_webhook() view - when you receive one of these responses from Stripe, does the logging look like what you would expect? IS there any logging? Where does it stop or differ from what’s expected?

It’s impossible for me to know whether the request you’re sending using Postman is failing for the same reason as an actual request fails.

The logs from my webhook view do NOT make it into CloudWatch at all. Nothing. That’s what makes this so puzzling.

I expected the Postman test to fail, which it did with a 400 Bad Request. However, THAT request actually makes it into my CloudWatch logs with this:

{
    "timestamp": "2025-02-27 13:39:17",
    "level": "ERROR",
    "logger": "apps.payments.views",
    "message": "Invalid Stripe signature: No signatures found matching the expected signature for payload",
    "path": "/app/apps/payments/views.py",
    "lineno": 283
}

It’s obviously saying Invalid Stripe signature, but I don’t know why. All of my keys are 100% correct.

Given the available information it sounds most like the webhook request isn’t reaching your site, hence no logging from the view. I’d guess it’s something to do with the AWS and load balancer stuff, but that’s something I know even less about!

As for the test failing, that feels like a separate problem, so I’d focus on one at a time, or you’ll be going mad (or more than you probably are :slight_smile: )

It’s obviously saying Invalid Stripe signature, but I don’t know why. All of my keys are 100% correct.

If they really are, then there’s an error in your code decoding/parsing the data. I’d be googling stripe.error.SignatureVerificationError and trying all the things people suggest for fixing that, e.g. this or this etc.

@philgyford yep. I agree. Thanks for jumping in! I’ll post here if / when I get this mystery solved.