Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Anotheruser's avatar

[L4] Cashier handleInvoicePaymentFailed not getting called

Hi.

I am having problems handling invoice.payment_failed events from Stripe. My handleInvoicePaymentFailed() in my custom WebhookController class does not get called. For now I simply need to add a little extra functionality after the event has been received. I need to update the 'active' field in the database for the user involved.

For this I have the following code in place.

Routes.php

Route::post('stripe/webhook','WebhookController@handleWebhook');

Custom WebhookController in app/controllers - see comments for the problem

<?php

class WebhookController extends \Laravel\Cashier\WebhookController {

    public function handleWebhook()
    {
        // If adding the update code here the Company is updated - I am using Company 1 just for testing for now
        $company = Company::find(1);
        $company->active = 0;
        $company->update();

        // Fallback to failed payment check...
        return parent::handleWebhook();
    }

    // This never gets called
    public function handleInvoicePaymentFailed(array $payload)
    {
        // This never happens
        $company = Company::find(1);
        $company->active = 0;
        $company->update();

    }
}

When I run an ' invoice.payment_failed' in stripe.com / accounts / webhooks with the following settings my handleInvoicePaymentFailed never gets called. Im unsure how to make that happen from within my class as I obviously only need it to occur at the appropriate time, ie and invoice payment failing.

Webhook settings in Stripe.com

URL: https://my-domain-here/stripe/webhook

Mode: Test

- Send me all events

Then send test webhook

Event: invoice.payment_failed

Version: 2014-11-20 (default)

I also tried changing 'Send me all events' to Select Events - Invoice.payment_failed. I was hoping that the default handleWebhook would then channel the event to my own handleInvoicePaymentFailed(). Instead i GET A 'Test webhook sent successfully' message but no changes in the database.

  • What should actually change in the database when this event is received? I thought it would cancel the account so that I was would then see a 'subscription_ends_at' date appear?

Finally I have also tried creating a route directly to the handleInvoicePaymentFailed() in my WebhookController

Route::post('stripe/webhook/handle-invoice-payment-failed', 'WebhookController@handleInvoicePaymentFailed');

In which case I get the following 500 error

Argument 1 passed to WebhookController::handleInvoicePaymentFailed() must be of the type array, none given

Any advice welcome, im out of ideas now.

Thanks.

0 likes
8 replies
Anotheruser's avatar

*** Update ***

Adding a check into the handleWebhook() in my WebhookController allows me to call my own handleInvoicePaymentFailed() like so:

<?php

class WebhookController extends \Laravel\Cashier\WebhookController {

    public function handleWebhook()
    {
        $payload = $this->getJsonPayload();

        if( $payload['type'] == 'invoice.payment_failed')
        {
            $this->handleInvoicePaymentFailed($payload);
        }

        // Fallback to failed payment check...
        return parent::handleWebhook();
    }

    // This never gets called
    public function handleInvoicePaymentFailed(array $payload)
    {
        // // This never happens
        $company = Company::find(1);
        $company->active = 0;
        $company->update();
      
        return new Response($payload, 200);
    }
}

Is a better way to do this - it feels like a bit of a hack.

nicogominet's avatar

Hi @appnorth like you I'm using the Company model for my Stripe subscriptions, but instead of adding an active field to the companies table, I chose to use the Cashier expired() method on an App::before() filter, like so:

if(Auth::check())
{
    // ...

    if(Auth::user()->company->expired()) return redirect(route('logout'));// I flash a message as well

    // ...
}

Wouldn't that be better for you as well?

Anotheruser's avatar

Hi nikospkrk

Thanks for your reply. I have only just seen it.

I don't suppose you can provide any more code around your solution?

Also, when the 'invoice.payment_failed' is sent from Stripe what should that actually do in the database? I had assumed that it would cancel the account in Stripe and add a 'subscription_ends_at' date in my database, that doesn't seem to happen. Im not sure if thats because i'm in Test mode at the minute or something else.

The thing is, with my system I need the user to be able to login but restrict what they can do after the 'invoice.payment_failed' event has occured. So for example, they can only access a single view which will inform them of the payment failure and a way to rectify it.

Thanks again.

Francismori7's avatar

\Laravel\Cashier\WebhookController@handleWebhook checks if the event really exists - thus TEST hooks will NOT be handled

        if (! $this->eventExistsOnStripe($payload['id'])) {
            return;
        }

Comment out return; and you'll be set for testing.

1 like
aaronhuisinga's avatar

@Francismori7 Your reply just saved me a ton of time. I absolutely could not figure out why my test webhooks weren't calling my extension functions!

handy_man's avatar

If you look in the Stripe account settings under "subscription" you should see how stripe handles failed payments, it retries them over a few days by default, you need to change this to

1st failed attempt: Stop trying to collect payment and... Then finally: Cancel subscription

This will send the Subscription deleted hook and trigger the CustomerSubscriptionDeleted function (which comes default with Cashier) This function should update your database and set Stripe active to false on the model and everything else.

cheah2go's avatar

I am using L5.1 and just went through this exact thing and what I found, at least in the version of Cashier that I'm working with, is that this code block:

        if (! $this->eventExistsOnStripe($payload['id']) && ! $this->isInTestingEnvironment()) {
            return;
        }

Will cause the method to do nothing and return nothing because the event id in a test webhook is not a real event id, and the second part will also evaluate as true unless you add the following to your .env file:

CASHIER_ENV=testing

as discovered by looking at the isInTestingEnvironment() method.

2 likes

Please or to participate in this conversation.