retro spiller

technokracy

Stripe Webhooks and CakePHP

Using and understanding Stripe’s webhooks can be a vital part of using Stripe for processing payments, especially when it comes to subscriptions or recurring billing. According to their docs:

Interacting with third party APIs like Stripe often suffers from two important problems:

  1. Services not directly responsible for making an API request may still need to know the response
  2. Some events, like disputed charges and many recurring billing events, are not the result of a direct API request

Webhooks solve these problems by letting you register a URL that we will POST anytime an event happens in your account.

As webhooks are just JSON requests POSTed to a receiving url you set in Stripe’s account settings, handling them are easy in CakePHP. No special routes need to be created, and the webhook request can be turned into a PHP object with one line of code:

1
2
<?php
$event_json = $this->request->input('json_decode');

The Stripe docs recommend using only the event id from the received webhook, and then retrieving the event via the Stripe API.

If security is a concern, or if it’s important to confirm that Stripe sent the webhook, you should only use the ID sent in your webhook and should request the remaining details from the Stripe API directly.

In the following example, the controller method will:

  1. Receive the webhook
  2. Turn it into a PHP object
  3. Get the event id
  4. Retrieve the event
  5. Do something based on what the event was
  6. Reply with an HTTP status code (200) telling Stripe the webhook was recieved

You’ll need either my CakePHP Stripe Plugin or the Stripe PHP API library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
public function processWebhook() {
  $this->autoRender = false;

  // Stripe sends webhooks as POST only
  if ($this->request->is('post')) {

      // If using my CakePHP Stripe plugin, these are set in Config/bootstrap.php
      $mode = Configure::read('Stripe.mode');
      $key = Configure::read('Stripe.' . $mode . 'Secret');
      // If not using the plugin, set $key to your secret API key
      Stripe::setApiKey($key);

      // This receives the request and runs json_decode, turning the JSON request into a PHP
      // object.
      $event_json = $this->request->input('json_decode');

      // Retrieve the event based on the event id, throw an exception and log what happened if the
      // event wasn't found or couldn't be retrieved. 
      try {
          $event = Stripe_Event::retrieve($event_json->id);
      } catch (Exception $e) {
          CakeLog::write('hook', 'No event found for: ' . $event_json->id);
          // We still return a HTTP 200 because the webhook was successfully recieved, even if the
          // event was invalid.
          return $this->response->statusCode(200);
      }

      // In this example, a User.stripe_id field exists in the users table, containing the Stripe
      // customer id. Retrieve the user based on the event's customer id. 
      $user = $this->User->findByStripeId($event->data->object->customer);

      if ($user) {
          // An invoice was paid. Send an email confirmation, update the database, etc.
          if ($event->type == 'invoice.payment_succeeded') {
              // do stuff for $user['User']['id']
          }

          // An invoice payment failed, email the user, etc.
          if ($event->type == 'invoice.payment_failed') {
              // do stuff for $user['User']['id']
          }

          // A customer's subscription was canceled at Stripe (invalid card or expired card,
          // etc.), cancel their account.
          if ($event->type == 'customer.subscription.deleted') {
              // cancel account for $user['User']['id']
          }
      }
      
      // Tell Stripe the webhook request was received so they don't try to resend it.
      return $this->response->statusCode(200);
  }
}

The full list of event types are in Stripe’s API docs. In the next part of this post I cover how to write tests.