Webhook: Payment Failed!

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Ok, there's just one more webhook we need to worry about, and it's the easiest one: invoice.payment_failed. Send a test webhook for this event.

Refresh RequestBin to check it out.

This webhook type is important for only one reason: to send your user an email so that they know we're having problems charging their card. That's it! We're already using a different webhook to actually cancel their subscription if the failures continue.

This has almost the same body as the invoice.payment_succeeded event: the embedded object is an invoice and if that invoice is related to a subscription, it has a subscription property.

That means that in WebhookController, this is a pretty easy one to handle. Add a new case for invoice.payment_failed:

... lines 1 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
... lines 16 - 31
switch ($stripeEvent->type) {
... lines 33 - 50
case 'invoice.payment_failed':
... lines 52 - 62
break;
... lines 64 - 66
}
... lines 68 - 69
}
... lines 71 - 90
}

Then, start just like before: grab the $stripeSubscriptionId. Then, add an if statement - just in case this invoice has no subscription:

... lines 1 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
... lines 16 - 31
switch ($stripeEvent->type) {
... lines 33 - 50
case 'invoice.payment_failed':
$stripeSubscriptionId = $stripeEvent->data->object->subscription;
if ($stripeSubscriptionId) {
... lines 55 - 60
}
break;
... lines 64 - 66
}
... lines 68 - 69
}
... lines 71 - 90
}

What to do when a Payment Fails?

Earlier, we talked about what happens when a payment fails. It depends on your Subscription settings in Stripe, but ultimately, Stripe will attempt to charge the card a few times, and then cancel the subscription.

You could send your user an email each time Stripe tries to charge their card and fails, but that'll probably be a bit annoying. So, I like to send an email only after the first attempt fails.

To know if this webhook is being fired after the first, second or third attempt, use a field called attempt_count. If this equals one, send an email. In the controller, add if $stripeEvent->data->object->attempt_count == 1, then send them an email. Well, I'll leave that step to you guys:

... lines 1 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
... lines 16 - 31
switch ($stripeEvent->type) {
... lines 33 - 50
case 'invoice.payment_failed':
$stripeSubscriptionId = $stripeEvent->data->object->subscription;
if ($stripeSubscriptionId) {
... lines 55 - 56
if ($stripeEvent->data->object->attempt_count == 1) {
... line 58
// todo - send the user an email about the problem
}
}
break;
... lines 64 - 66
}
... lines 68 - 69
}
... lines 71 - 90
}

If you need to know which user the subscription belongs to, first fetch the Subscription from the database by using our findSubscription() method. Then, add $user = $subscription->getUser():

... lines 1 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
... lines 16 - 31
switch ($stripeEvent->type) {
... lines 33 - 50
case 'invoice.payment_failed':
$stripeSubscriptionId = $stripeEvent->data->object->subscription;
if ($stripeSubscriptionId) {
$subscription = $this->findSubscription($stripeSubscriptionId);
if ($stripeEvent->data->object->attempt_count == 1) {
$user = $subscription->getUser();
// todo - send the user an email about the problem
}
}
break;
... lines 64 - 66
}
... lines 68 - 69
}
... lines 71 - 90
}

I like this webhook - it's easy! And actually, we're done with webhooks! Except for preventing replay attacks... which is important, but painless.

Leave a comment!

This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9, <7.4",
        "symfony/symfony": "3.1.*", // v3.1.10
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.8
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
        "stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
        "stripe/stripe-php": "^3.15", // v3.23.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.2.1
        "phpunit/phpunit": "^5.5", // 5.7.20
        "twig/twig": "^1.24.1" // v1.35.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.4
        "symfony/phpunit-bridge": "^3.0", // v3.3.0
        "hautelook/alice-bundle": "^1.3", // v1.4.1
        "doctrine/data-fixtures": "^1.2" // v1.2.2
    }
}