Straightforward way to integrate Stripe using Go

Guilherme Pereira
8 min readFeb 20, 2024

--

Let’s help you not to drown into the vast Stripe documentation.

Photo by Mark König on Unsplash

Preface

This article came as a need when I was integrating Stripe into a project. Although it has a good documentation, it is too bloated and you can get lost. I felt the need of something that would give me all the info that I needed for that specific situation I was in, in just one place.

The aim of this article is to provide a simple way of integrating Stripe into your Go application, while also having all the knowledge you need to do it (and not ALL the information existent).

💻 The code is available in the GitHub if you just want to dive into it. There you will also find some documentation on how to start using Stripe in test mode and other information. I would advise reading through this article before and only then going there.

⚠️ Also, be aware to be testing things on the test mode and not live mode, as the latter can incur in costs depending on the type of testing you’re doing. ⚠️

Intro

Stripe is a widely known world wide payments platform that is broadly used in the industry. It also has an enormous documentation that helps users do whatever they want with the system.

There’s a problem though: when there’s too much of something, you can find yourself drowning in information.

Knowing what you need and having them in a single place is the best way of dealing with this problem. So let’s start.

About Stripe

Stripe works with the concept of webhooks, which are events that Stripe sends us — to an endpoint in our own application— describing something that has happened in their platform. Examples of these events are: invoice was paid, subscription was created, customer details were updated, subscription was cancelled, and so on.

These webhooks are the way we do something on our side of the application in reaction to something that happened on their side. For example, you may want to know when an user subscribed to your platform so you can do something in your side, like saving its credentials in the database.

Knowing which events to listen to and which events are triggered in response to determined actions are the epicenter in understanding the application flow.

What we’re trying to solve

We have an application that provides some value to a client. This app will work on a subscription model, which means that it’s going to require a payment every X months.

The application can also have a trial mode in which the user will be able to test the platform for Y days without having to pay for it.

During a valid trial period, the subscription status is set to "trialing".

After the trial is over, if the user didn’t cancel the subscription, it will generate an invoice so he can pay for it and start its new X months journey until the next billing cycle, when it will then renew the subscription for more X months.

During a valid subscription period, the subscription status is set to “active”.

To create a Stripe subscription, you can go to the Subscriptions in the Dashboard and just click on “Create subscription”.

When the user first subscribes to our platform, we will have to do some things in order to validate his access, for example:

  • Create a new user with a random password;
  • Update this user with the expire date of the current subscription cycle;
  • Send the new credentials to the user via email.

These are simple examples that usually platforms do when they register users with a subscription model.

Modelling our problem

Before writing code, is always good to first model what we need to accomplish.

Subscription

We may say that we have something like the following:

Our application subscription model

So, from above, we can see the whole subscription flow in order to first register an user in the platform. What really matters in this part is if we receive the event invoice.paid, which basically says: you’re free to go. The subscription was paid successfully. Release the Kraken!

Cancellation

And here we have our cancellation flow, which basically deletes the user subscription:

Cancellation flow

The flow above triggers the customer.subscription.deleted event. It will effectively remove the user from Stripe customers: he will no longer be part of the platform.

Therefore, if an user cancels its subscription and then after some time decides that he wants to come back, a brand new subscription will have to be created for him.

In the case that an user chooses to cancel its subscription at the end of the current billing cycle, the customer.subscription.deleted event will not be triggered, but the customer.subscription.updated. In this case, if your application uses the expiration date of the subscription as a means to check if the user is allowed to use the platform or not, it won’t affect its usage until the end of the period, when it effectively will end and a customer.subscription.deleted will then be triggered.

Payments renewal

The renewal of payments is handled by Stripe, but depending on how you approach the options of payments, it will require a bit more complexity.

This article assumes that the payment methods you’re accepting are credit cards, Google Pay and Apple Pay. You can check those under the configurations of the billing cycle.

This simplifies a lot of the payment verification because it can all be handled by Stripe. If you choose something that will send the invoice to the customer and then wait for him to pay, that will require a bit more complexity and time to start using the application — and can also be bad for business if the soon-to-be customer decides that it’s too much hassle to pay for something.

Once the current billing cycle ends, Stripe will request a payment automatically and, if everything goes well, the status of the subscription will be active, invoice will be paid and the expiration date will be update to the end of next billing cycle.

In the case that a payment is not successful — and the reasons for that are:

  • Lack of a payment method on the customer;
  • The payment method is expired;
  • The payment is declined.

Stripe will try again using what’s called Smart Retries, which basically will pick automatically the gap between retries and at which specific hours will they be made.

If, after all, the payment remains unsuccessful, it will cancel the customer subscription and set his unpaid invoice status to uncollectible.

You can change these options — including the Smart Retries — under the automatic billing configurations.

Bundling it all together

Using these models that we have for subscription, cancelling and payment renewals, we find ourselves with the following diagram (may require zooming a bit):

Application flow

Building the Webhook

When creating the endpoint in our application that will handle the events from Stripe — or, in other words, our webhook— , there are some best practices to ensure that everything works well and that we are dealing with events from Stripe itself and not from someone else trying to hijack our platform.

There are some measures, like idempotency, that are not covered here. But some of them are:

Allowed IPs

Stripe maintains a pool of IPs that we can check once a request is made to our system and validate if it is from them. You can find those IPs here.

Here’s how we can validate those using Go and the Echo web framework:

package handlers

import (
"fmt"
"io"
"net/http"
"os"
"slices"

"github.com/labstack/echo/v4"
"github.com/stripe/stripe-go/v76/webhook"
)

var AllowedStripeIPs = [...]string{
"3.18.12.63",
"3.130.192.231",
"13.235.14.237",
"13.235.122.149",
"18.211.135.69",
"35.154.171.200",
"52.15.183.38",
"54.88.130.119",
"54.88.130.237",
"54.187.174.169",
"54.187.205.235",
"54.187.216.72",
"::1", // for local testing
}

func WebhookHandler(c echo.Context) error {
req := c.Request()
res := c.Response()
ipFromStripeWebhook := c.RealIP()

// Checks webhook coming from allowed IP
if !slices.Contains[[]string](AllowedStripeIPs[:], ipFromStripeWebhook) {
fmt.Fprintln(os.Stderr, "You shall not pass")
err := echo.ErrBadRequest
err.Message = "IP not allowed"
return err
}

// ...
}

If any request that comes to our webhook have an IP that is not in the pool of allowed ones, it won’t proceed (or, should I say, You shall not pass!!!).

Webhook signature

Another great thing that Stripe has is the concept of webhook signatures. It is yet another layer of security to ensure that you’re dealing with them and not someone else. This is how you do it:

 // ...

// Checks webhook signature.
// This makes sure that the POST request is actually coming from Stripe.
stripeWebhookSecret := os.Getenv("STRIPE_WEBHOOK_KEY")
signatureHeader := req.Header.Get("Stripe-Signature")

const MaxBodyBytes = int64(65536)
req.Body = http.MaxBytesReader(res.Writer, req.Body, MaxBodyBytes)

body, err := io.ReadAll(req.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
res.WriteHeader(http.StatusServiceUnavailable)
return err
}

event, err := webhook.ConstructEvent(body, signatureHeader, stripeWebhookSecret)
if err != nil {
fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err)
res.WriteHeader(http.StatusBadRequest)
return err
}

// ...

The STRIPE_WEBHOOK_KEY can be found, in test mode, when you run the stripe listen command — check the GitHub repository for more information. For the live mode, you will need to get it at Developers — Webhook.

The webhook.ConstructEvent is a validation helper function provided by the Stripe Go package “github.com/stripe/stripe-go/v76/webhook”.

Think of this validation as a JWT validating tokens. It has a header, a body and a signature. If it is not valid, it won’t proceed (or, should I say, You shall not pass!!! — yes, I did it again).

Acknowledge events immediately

From the docs:

“Your endpoint must quickly return a successful status code (2xx) prior to any complex logic that could cause a timeout. For example, you must return a 200 response before updating a customer’s invoice as paid in your accounting system.”

This is not a measure of security, but rather to tell Stripe that their job is done. Anything that may go wrong after the webhook was sent, it’s on us, the application, to deal with it.

This is how you can do it in Go:

 // ...

// Use a goroutine so we can acknowledge events immediately
go application.CheckEventTypes(res, event)

res.WriteHeader(http.StatusOK)
return nil

// ...

Conclusions and directions

This article will be complemented by the GitHub repository. There you will find, amongst other things: the code, instructions on how to run Stripe in test mode, what you need to initiate, and even some QA about some doubts I had during the process.

The goal is to read this article to have broader understanding on how Stripe works, and what the repository was modelled after, and then jump into the code and run it.

I tried to use simple language in the most detailed yet direct way possible. But, in case of any doubts, leave it in the comments section.

Stay cheerful and constructive!

--

--