Better Programming

Advice for programmers.

Follow publication

Integrating Recurring Payments to Your Rails API With Stripe

In this article I will go through a Subscription Integration in a Rails API, using the Stripe API it.

Bruno Feres
Better Programming
Published in
9 min readJan 31, 2022

--

For this application, I will consider you already have a User’s model in your application. In case you don’t have it yet, don’t worry! I already published an article about that, you can read it here.

Getting Started

If you followed the steps of my other article, to create your User model, or simply cloned it from GitHub, you will need to change the Ruby version on the Gemfile. For this article, I will be using Ruby 3.0.0, so your Gemfile must look like that:

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '3.0.0'...

Also, add the Stripe Gem to your Gemfile.

gem 'stripe'

Don’t forget to run bundle install in your terminal after that.

Now you will need to create a stripe initializer file in your application. For that, run the command below in your terminal:

touch config/initializers/stripe.rb

Then, open the stripe.rb file and write the content below:

Stripe.api_key = Rails.application.credentials.stripe_secret_key

We didn’t add that Stripe’s secret key in our credential file, so let’s do this.

To open your credential file, run in your terminal:

EDITOR=nano rails credentials:edit

Get your Stripe Secret Key here.

Your credential file must look like this:

# aws:
# access_key_id: 123
# secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: [YOUR SECRET KEY BASE]
stripe_secret_key: [YOUR SECRET KEY HERE]

Note that the secret_key_base is autogenerated, you don’t need to edit.

The User Model

First of all, we need our users to have a Stripe Customer ID. We need to store that ID in our database, so run the command below to create a migration:

rails g migration AddStripeIdToUsers stripe_id
rails db:create db:migrate

We will also add a new validation to our User, to make sure the Stripe ID will be always present. Add to your User model:

validates :stripe_id, presence: true, uniqueness: true

To create a reference of our User on Stripe API, let’s add a callback to User model.

Add the code below to the User model.

...  before_validation :create_stripe_reference, on: :create  def create_stripe_reference
response = Stripe::Customer.create(email: email)
self.stripe_id = response.id
end
end

Would be pretty interesting to have a method to retrieve the stripe information about the customer if we need it. Fortunately, Stripe SDK provides this method, which we will add to our User model too.

...  def retrieve_stripe_reference
Stripe::Customer.retrieve(stripe_id)
end
end

Finally, your User model should look like this:

class User < ApplicationRecord
has_secure_password
validates :email, uniqueness: { case_sensitive: false }
validates :stripe_id, presence: true, uniqueness: true
before_validation :create_stripe_reference, on: :create def create_stripe_reference
response = Stripe::Customer.create(email: email)
self.stripe_id = response.id
end
def retrieve_stripe_reference
Stripe::Customer.retrieve(stripe_id)
end
end

Awesome!! Let’s try our code at the console, run in your terminal:

rails c

You can now create a User, and the code will automatically contact Stripe API and create the reference.

User.create(email: "user@test.com", password: "123456")

You can check out the Stripe ID of this user with the commands below.

user = User.first
user.stripe_id

And to retrieve all the Stripe info for this user, run:

user.retrieve_stripe_reference

That looks fantastic.

The Plan Model

The Plan model will be responsible for representing and handling business rules for the subscription options we will offer.

Run-on your terminal:

rails g model Plan name description interval:integer price_cents:integer stripe_price_id
rails db:migrate

Take a closer look at the Plan attributes:

  • Name will be a string and represents the name of the plan;
  • Description will be a string and represents short text, describing what the plan offers;
  • Interval will be an integer and represents the interval of charging the subscriber;
  • price_cents will be an integer and represents the value for the subscription, it needs to be cents because Stripe works with cents values and it is easier to handle the value this way.
  • stripe_price_id will be a string and represents a reference to our Plan in the Stripe API.

That’s how the Plan model will look like:

class Plan < ApplicationRecord
validates :name,
:stripe_price_id,
:price_cents, presence: true
validates :name,
:stripe_price_id, uniqueness: true
enum interval: %i[month year] before_validation :create_stripe_reference, on: :create def create_stripe_reference
response = Stripe::Price.create({
unit_amount: price_cents,
currency: 'usd',
recurring: { interval: interval },
product_data: { name: name }
})
self.stripe_price_id = response.id
end
def retrieve_stripe_reference
Stripe::Price.retrieve(stripe_price_id)
end
end

Take a closer look at the Plan model:

  • First, we defined some validations, to make sure name, stripe_price_id and price_cents will be always present. We also defined a validation to make sure the name and the stripe_price_id will be unique.
  • After that, we defined our interval enum, rails will handle those options, so when we pass the value "month" for the interval, the framework will store 0 in the database and when we pass the value "year" it will store 1.
  • Then we defined a callback to create a reference to our Plan on Stripe API.
  • The create_stripe_reference method will be responsible for the all the logic triggered on the callback.
  • The retrieve_stripe_reference will basically return the Stripe reference of our Plan.

Let’s try our new model on the console. Run in your terminal:

rails c

You can now create a Plan, and the code will automatically contact Stripe API and create the reference:

# Month Subscription
Plan.create(
name: 'Month Subscription',
description: 'This subscription will be charged every month',
interval: 'month',
price_cents: 500
)
# Year Subscription
Plan.create(
name: 'Year Subscription',
description: 'This subscription will be charged every year. Save $10 with this subscription.',
interval: 'year',
price_cents: 5000
)

To retrieve all the Stripe info for some plans, run:

# The 1 represents the ID of the record.
Plan.find(1).retrieve_stripe_reference

The Stripe Price Docs.

The Subscription Model

The Subscription model will be responsible for representing and handling business rules for a user’s subscription.

Run the following command on your terminal:

rails g model Subscription plan:references user:references active:boolean stripe_id
rails db:migrate

Taking a closer look to the Subscription attributes:

  • plan will be a reference to the plan that is being purchased;
  • user will be a reference to the user that is purchasing a plan;
  • active will be a boolean that represents if a subscription is still valid.
  • stripe_id will be a string and represents a reference to the Subscription in the Stripe API.

Open the migration create_subscriptions and make sure to define the default value of the active attribute:

...
t.boolean :active, default: true
...

After that, let’s code the Subscription model. That’s how it should look like:

class Subscription < ApplicationRecord
attr_accessor :card_number, :exp_month, :exp_year, :cvc
belongs_to :plan
belongs_to :user
validates :stripe_id, presence: true, uniqueness: true before_validation :create_stripe_reference, on: :create def create_stripe_reference
Stripe::Customer.create_source(
user.stripe_id,
{ source: generate_card_token }
)
response = Stripe::Subscription.create({
customer: user.stripe_id,
items: [
{ price: plan.stripe_price_id }
]
})
self.stripe_id = response.id
end
def generate_card_token
Stripe::Token.create({
card: {
number: card_number,
exp_month: exp_month,
exp_year: exp_year,
cvc: cvc
}
}).id
end
end

Take a closer look at the Subscription model:

  • First of all, we defined some virtual attributes. We will not store that data, because it is sensitive. So Stripe will handle it for us.
  • After that, we defined the relations of the model, a belongs to Plan and User.
  • The only attribute we will validate is stripe_id, it must be present and be unique.
  • We also have a callback, that will be triggered before the validation of the record creation.
  • The create_stripe_reference will be responsible for creating the reference of our subscription on the Stripe API. Note that before creating the subscription on Stripe API, we create a source for the customer, that will be the payment method.
  • The generate_card_token will be responsible for passing the card data to Stripe API and return a token. Passing a token is more secure than passing the card data, that’s why Stripe handles the payment source this way.

Let’s try our new model on the console. Run in your terminal:

rails c

You can now create a Subscription, and the code will automatically contact Stripe API and create the reference.

Subscription.create(
card_number: '5555555555554444',
exp_month: 06,
exp_year: 2029,
cvc: '123',
user_id: User.first.id,
plan_id: Plan.first.id,
active: true
)

Note that Stripe has pre-defined allowed test card numbers. You can check it out here: Stripe Test Cards.

That’s awesome!! Our Subscription is already running and you can see it on the Stripe Dashboard.

Stripe Dashboard

Canceling a Subscription

Sometimes we need to save money and some subscriptions must be canceled, let’s program the application to allow cancelations.

What we will do is basically update the record and the reference on Stripe API when active is false. Let's code it.

Add a callback to cancel the subscription on Stripe API, with activeness conditional.

...before_update :cancel_stripe_subscription, if: :subscription_inactive?...

We also need to add the method that the callback call and the method that define the condition.

...  def cancel_stripe_subscription
Stripe::Subscription.delete(stripe_id)
end
def subscription_inactive?
!active
end
end

To test if it works, go to your rails console and run the commands below:

Subscription.first.update(active: false)

Now the subscription is canceled, and you can check it on your Stripe Dashboard.

Stripe Dashboard

What about the Controllers?

For this post, we will have controllers only to show, create and update subscriptions. Once the Plans CRUD is more an administrative thing, I will not create a controller for it now.

The Subscriptions Controller

To create the subscriptions controller, run the command below in your terminal:

rails g controller subscriptions show create update

The command above will add some routes to our routes file, but we need to change it, once we want the routes inside the namespace api/v1.

So your routes may look like this:

Rails.application.routes.draw do
namespace :api do
namespace :v1 do
post '/signup', to: 'users#signup'
post '/signin', to: 'users#signin'
get '/signedin', to: 'users#signedin'
resources :subscriptions, only: %i[show create update]
end
end
end

After generating the controller, let’s write it. Since that’s a very generic rails controller, I will not explain the code of it in this article.

Your controller should look like this:

module Api
module V1
class SubscriptionsController < ApplicationController
before_action :set_subscription, except: %i[create]
def show
render json: @subscription
end
def create
@subscription = Subscription.new(subscription_params)
if @subscription.save
render json: @subscription, status: :created
else
render json: @subscription.errors, status: :unprocessable_entity
end
end
def update
if @subscription.update(subscription_params)
render json: @subscription
else
render json: @subscription.errors, status: :unprocessable_entity
end
end
private def set_subscription
@subscription = Subscription.find(params[:id])
end
def subscription_params
params.require(:data).permit(:card_number, :exp_month, :exp_year, :cvc, :user_id, :plan_id, :active)
end
end
end
end

Testing the Controller — Requests

I will use Paw to run the requests and test the controller, but you will achieve the same results with any HTTP Client, like Postman or Insomnia.

Up your local server with the command below:

rails s

The Create Request

This request will trigger a subscription creation.

Action: POST

Endpoint:

http://localhost:3000/api/v1/subscriptions

Headers:

Content-Type: application/json

Body:

{
"data": {
"card_number": "5555555555554444",
"exp_month": "09",
"exp_year": "2029",
"cvc": "123",
"user_id": "1",
"plan_id": "1"
}
}

The response to this request should match the format below:

Create Response

The Update Request

This request will cancel a subscription.

Action: PUT or PATCH

Endpoint:

http://localhost:3000/api/v1/subscriptions/3

The number 3 on the endpoint above, represents the ID of the record we want to update, you may change it related to the ID of your subscription.

Headers:

Content-Type: application/json

Body:

{
"data": {
"active": false
}
}

The response of this request should match the format below:

Update Response

The Show Request

This request will retrieve a subscription.

Action: GET

Endpoint:

http://localhost:3000/api/v1/subscriptions/3

The number 3 on the endpoint above, represents the ID of the record we want to update, you may change it related to the ID of your subscription.

The response of this request should match the format below:

Show Response

Conclusion

Stripe make the payment system integration be pretty simple and easy.

There are a lot of resources on Stripe API that were not explored during this article. I mean to publish another one in the future, related to payment intent and webhooks for receiving the intent to update the status of a subscription.

You can check the complete Stripe Docs here.

Want to Connect?Connect on Twitter.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Bruno Feres
Bruno Feres

Written by Bruno Feres

Sr. Software Engineer, obsessed reader and silly jokes teller.

Responses (5)

Write a response