Testing stripe webhook locally - Stripe webhook localhost

How to receive Stripe webhooks on your localhost for development and testing

Testing Stripe webhooks locally on your localhost machine is easy as long as you have the right set of tools to create a publicly reachable URL for your localhost app.  In this article, I’ll demonstrate how to receive and test stripe webhooks locally on your localhost using a simple demo app.

What is Stripe Webhook

Stripe is one of the major online payment gateway and is a great alternative to paypal. It is a preferred choice for SaaS, e-commerce businesses as well as developers like us. Stripe is popular because it has an easy to use APIs, SDKs in many different programming languages(Ruby, NodeJS, Go, Python, Java etc.) and excellent documentation.

Stripe, like other online payment platforms, utilizes webhooks to inform the seller’s application about customers, product subscriptions, payment cards and other events occuring in the Stripe payment processing system. Receiving and processing Stripe webhooks are critical for building subscription (recurring payments) based payment gateway application where your backend system needs to track subscription status,

What is the problem

While it is easy to receive and process Stripe webhooks in production systems (because production systems are mostly customer facing public servers with public IPs), it is extremely challenging to receive and process Stripe webhooks on your local machine during development and testing. Your development and test machines are present in the local network.  Your payment gateway application under development runs on your localhost at some port 8080.  Stripe online cannot reach your localhost app.

In this article, I’ll demonstrate how to receive and test Stripe webhooks locally on a laptop using a demo application running on a localhost network.

What you’ll learn from this article:

  • Build a simple application to receive and process Stripe webhooks for subscription change events.
  • We will use SocketXP webhook proxy service to relay Stripe webhooks to the demo app run locally
  • We will use Stripe’s webhooks testings dashboard to simulate subscription change events.
  • We will use the SocketXP’s webhooks dashboard to monitor and replay the cached webhooks as they pass through the reverse proxy tunnel.

Before we get started

Here are some of the prerequisites for this demo.

The demo application

Our demo application is written Go and it simply does the following:

  • The app listens on localhost port 8080 and handles any incoming HTTP requests for the subdirectory path “/stripe”.
  • It first verifies the signature of the Stripe webhook notification using the Stripe library method “webhook.ConstructEvent()” to validate the authenticity of the webhook source. For this you need to set an environmental variable named “STRIPE_SECRET” with your stripe secret retrieved from the Stripe Webhooks Dashboard.
  • It processes 3 different Stripe webhook events related to customer subscription.
  • It prints select details from the webhooks received such as CustomerID, Quantity and Status of the subscription.
  • It throws an error if the select details are missing in the webhooks received.

Download the demo application from GitHub: https://github.com/socketxp-com/stripe-webhook-demo

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"

    "github.com/stripe/stripe-go/webhook"
)

// application server port 
const port = ":8080"

func main() {
    secret := os.Getenv("STRIPE_SECRET")
    if secret == "" {
        fmt.Println("Required STRIPE_SECRET env variable is missing")
        os.Exit(1)
    }

    // incoming stripe webhook handler
    http.HandleFunc("/stripe", func(resp http.ResponseWriter, req *http.Request) {
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
            resp.WriteHeader(http.StatusBadRequest)
            return
        }

        // validating signature
        event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), secret)
        if err != nil {
            resp.WriteHeader(http.StatusBadRequest)
            fmt.Printf("Failed to validate signature: %s", err)
            return
        }

        switch event.Type {
        case "customer.subscription.created":
            // subscription create event
            customerID, ok := event.Data.Object["customer"].(string)
            if !ok {
                fmt.Println("customer key not found in event.Data.Object")
                return
            }

            subStatus, ok := event.Data.Object["status"].(string)
            if !ok {
                fmt.Println("status key not found in event.Data.Object")
                return
            }

            quantity, ok := event.Data.Object["quantity"]
            if !ok {
                fmt.Println("quantity key not found in event.Data.Object")
                return
            }

            fmt.Printf("customer %s subscription created, quantity (%f), current status: %s \n", customerID, quantity, subStatus)

        case "customer.subscription.updated":
            // subscription update event
            customerID, ok := event.Data.Object["customer"].(string)
            if !ok {
                fmt.Println("customer key not found in event.Data.Object")
                return
            }

            subStatus, ok := event.Data.Object["status"].(string)
            if !ok {
                fmt.Println("status key not found in event.Data.Object")
                return
            }

            quantity, ok := event.Data.Object["quantity"]
            if !ok {
                fmt.Println("quantity key not found in event.Data.Object")
                return
            }

            fmt.Printf("customer %s subscription updated, quantity(%f), current status: %s \n", customerID, quantity, subStatus)
        case "customer.subscription.deleted":
            // subscription deleted event 
            customerID, ok := event.Data.Object["customer"].(string)
            if !ok {
                fmt.Println("customer key not found in event.Data.Object")
                return
            }

            subStatus, ok := event.Data.Object["status"].(string)
            if !ok {
                fmt.Println("status key not found in event.Data.Object")
                return
            }

            quantity, ok := event.Data.Object["quantity"]
            if !ok {
                fmt.Println("quantity key not found in event.Data.Object")
                return
            }

            fmt.Printf("customer %s subscription deleted, quantity(%f), current status: %s \n", customerID, quantity, subStatus)
        default:
            fmt.Printf("Unknown event type received: %s\n", event.Type)
        }
    
    })

    fmt.Printf("Listening for Stripe webhooks on http://localhost%s/stripe \n", port)
    // starting the http server
    log.Fatal(http.ListenAndServe(port, nil))

}

Downloading and executing the demo application:

Assuming that you have already installed Golang on your machine, execute the below commands to download, set the STRIPE_SECRET environmental variable and run the demo application.

$ git clone https://github.com/socketxp-com/stripe-webhook-demo.git
$ ls
stripe-webhook-demo
$ cd stripe-webhook-demo/
$ ls
README.md main.go
$ export STRIPE_SECRET=”whsec_…”
$ go run main.go

The demo app listens on “localhost:8080”. We cannot use this local address for registering with Stripe to receive webhooks for development and testing purposes. We need a public IP address or domain name to receive Stripe webhooks. And that’s where the SocketXP webhooks proxy and relay service comes in handy.

What is SocketXP

SocketXP is a reverse proxy tunneling service that relays webhook notifications from online web services such as Stripe, Paypal, GitHub, DockerHub etc to applications running in localhost network behind NAT and Firewall.

SocketXP is a freemium SaaS service that significantly reduces the development and testing cycle for software teams, resulting in cost savings.

How SocketXP webhook proxy and relay service works

Install the SocketXP agent on your localhost machine where your app(webhook receiver) is developed and tested.  It could be installed on your laptop or your office LAN server.  The SocketXP agent creates a secure SSL/TLS tunnel to the SocketXP Cloud Gateway.  The SocketXP Cloud Gateway creates a unique secure (HTTPS) public endpoint for each user.  The user can use the SocketXP public endpoint (URL) to register with online webhook sender applications such as Stripe, Paypal etc.

When the webhook sender sends a webhook message on the SocketXP public endpoint(URL), SocketXP Cloud Gateway will validate the URL and then forward the webhook to the corresponding registered user’s SocketXP agent via a secure (SSL/TLS) tunnel.  The SocketXP agent would in turn forward the webhook message to the receiver application running in the localhost.  Learn more about how SocketXP solution works here.

SocketXP Cloud Gateway also caches the webhooks locally for your auditing and analysis needs.  Secondly, and more importantly, you could resend failed webhook notifications from the SocketXP dashboard without requesting the sender application to resend the webhook.  This feature is extremely useful during development and testing cycle.

Download Install and Login

Download and install the SocketXP agent for your OS version and Platform, and authenticate the SocketXP agent to the SocketXP Cloud Gateway by following the instructions here.

Create SocketXP public webhook relay endpoint

$socketxp relay http://localhost:8080/stripe

Connected.
Public URL -> https://webhook.socketxp.com/test-user-gmail-com-45803

Use this SocketXP public webhook endpoint (URL) to register with Stripe Webhook Testing Dashboard, explained in the next section.

Sending test webhooks from Stripe dasboard

Login to your Stripe account

Toggle the “view test data” button on the side navigation bar to put Stripe on a test mode.

Select the “webhooks” under the head “Developers” on the side navigation bar or go to https://dashboard.stripe.com/test/webhooks

 

Testing Stripe webhooks locally stripe webhooks localhost
Stripe Webhooks Testing Dashboard

 

Click the “Add endpoints” button on the right hand side window.

Add the SocketXP public webhook URL to the “Endpoint URL” text box. Choose the Stripe webhook events “customer.subscription.created”, “customer.subscription.updated”, “customer.subscription.deleted” from the “Events to send” drop down menu box right below it. Finally, click the “Add endpoint” button below to save the endpoint.

Now click open the endpoint you just added. And click the “Send test webhook” button on the top. Choose the Stripe webhook events, you wish to send and click the “Send test webhook” button.

Our demo app should receive webhook events from the Stripe webhook dashboard.

Demo app output:

The demo app receives webhooks from the SocketXP agent running locally on the same machine.

$go run main.go
Listening for Stripe webhooks on http://localhost:8080/stripe
customer cus_00000000000000 subscription created, quantity(1.000000), current status: active
customer cus_00000000000000 subscription updated, quantity(1.000000), current status: active
customer cus_00000000000000 subscription deleted, quantity(1.000000), current status: canceled

Conclusion

Stripe is a great payment gateway for developers to integrate because of its easy to use APIs and SDKs. Developing, integrating and testing your backend application with Stripe webhooks dashboard on your local machine could be a nightmare.  SocketXP webhook proxy service and the record/replay option will significantly reduce the development and testing time (and ofcourse, the cost) for your software team.

Leave a Reply

Your email address will not be published. Required fields are marked *