Stripe Events & Webhooks

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

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Oh boy, it's finally time to talk about the big subject I've been avoiding: webhooks. You see, every month Stripe is going to try to renew each customer's subscription. If it fails to charge their card, you might want to send them an email. If it's successful, you might want to send them a receipt. Or, if it fails so many times that the subscription needs to be cancelled, then we need to update our database to reflect that.

In short, Stripe needs to communicate back to us when certain things, or events happen.

Stripe Events

Go to our Customer page in Stripe. At the bottom, you'll see an events section. Basically, whenever anything happens in Stripe, an event is created. For example, when we added a new card, there was an event whose type field is set to customer.source.created. Every action becomes an event and there are many different types of events for the many things that can happen.

In fact, switch over to Stripe's API docs. Event is so important that it's an object in the API: you can list and fetch them. Click Types of events. Awesome! A big, giant, beautiful list of all the different event types so you can figure out what each event means.

What Happens when we Can't Charge a User?

Out of this list, there are just a few types that you'll need to worry about. The first is the event type that occurs when the customer's subscription is canceled when Stripe can't charge their card for renewal.

So, what actually happens when Stripe can't charge a card? Go back to the Stripe Dashboard and go to "Account Settings", and then "Subscriptions". This screen is really important: it determines exactly what happens when a card can't be charged. By default, Stripe will attempt to charge the card once, then try again 3, 5 and 7 days later. If it still can't charge the card, it will finally cancel the subscription. You can tweak the timing, but the story is always this: Stripe tries to charge a few times, then eventually cancels the subscription.

Hello Webhooks &

When this happens, we need Stripe to tell us that the subscription was canceled. And we'll do this via a webhook. It's pretty simple: we configure a webhook URL to our site in Stripe. Then, whenever certain event types happen, Stripe will send a request to our URL that contains the event object as JSON. So if Stripe sent us a webhook whenever a subscription was canceled, we would be in business!

A really nice way to test out webhooks is by using a site called


The site is no longer available (see Try instead that has a bunch more features.

Click "Create a RequestBin". Ok, real simple: this page will record and display any requests made to this temporary URL.

Back on our dashboard, add a webhook and paste the URL. Put it in test mode, so that we only receive events from the "test" environment. Next, click "Select events". Instead of receiving all event types, let's just choose the few we want. For now, that's just customer.subscription.deleted.

Yep, this is the event that happens when a subscription is cancelled, for any reason, including when a user's card couldn't be charged.

Create that webhook! Ok, let's see what a test webhook looks like. Click "Send test webhook" and change the event to customer.subscription.deleted. Now, send that webhook!

Refresh the RequestBin page. So cool! This shows us the raw JSON request body that was just sent to us. These events are just objects in Stripe's API, like Customer, Subscription or anything else. But if you configure a webhook, then Stripe will send that event to us, instead of us needing to fetch it from them.

Here's the next goal: setup a route and controller on our site that's capable of handling webhooks and doing different things in our system when different event types happen.

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