If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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 | |
} |
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.
Hey Chris,
Stripe prepare an upcoming invoices for subscriptions before each renew. And I believe that's where that subscription field is set, and that's what "The subscription that this invoice was prepared for, if any" means. But you somehow created a *test* webhook and it returned at this field a value of null that makes sense, i.e. this invoice wasn't prepared automatically by Stripe. I believe that's the root problem why subscription is null. I think you can try to successfully subscribe in test mode, then entering test credit card credentials - the subscription will be created and invoice will be paid. Then you can remove the credit card in the customer and on the next renewal I think you will get an "invoice.payment_failed" webhook and subscription won't be null.
I hope this helps, otherwise, I think you can contact Stripe support directly.
Cheers!
// 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
"composer/package-versions-deprecated": "^1.11" // 1.11.99
},
"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
}
}
Hey guys,
maybe you can help me :) At the moment I do setup my webhooks, and I'm following this lecture.
I understand the logic that we query only for invoices where actually a subscription was involved.
But why do we look for $stripeEvent->data->object->subscription?
In Stripe I created a test webhook and it returned at this field a value of null. Contrary, it returned within the invoice items array a subscription item.
This is just an excerpt of the line item's array
And further below in the invoice event:
When I look into the API https://stripe.com/docs/api#invoice_object I don't understand the field description of event.data.object.subscription: 'The subscription that this invoice was prepared for, if any.'
According to our setup the subscription filter would not trigger, although within the line Items there is a subscription.