Stripe is a service where retailers can create products with multiple prices, record customer data, and can keep track of what the customers are buying. You can use Stripe as a checkout tool, making your life as a business owner a bit simpler.
Why use an external checkout service? When you use an integration like Stripe, your web applications don’t have to record and manage sensitive data like credit card numbers. Stripe has great documentation, and developers don’t have to invest a lot of time and effort to get a working store up and running securely.
If that’s the case, why am I writing this blog post? Isn’t everything so easy and documented? Well, there are two main reasons:
- The examples I’ll be covering focus on separate parts, as well as on a fully working example that can help you prevent errors
- Stripe documentation uses the Sinatra framework, so Ruby on Rails examples are a great addition
Before integrating Stripe, let's build a simple shop web application that will sell some products. This will enable you to implement Stripe checkout in your Ruby on Rails app.
The Idea Behind the Online Shop
Normally, shops work like this: a provider is selling and a consumer is buying. In this example, let's create something different. One side will provide a technical solution. To be more precise, the provider will create a platform, and the consumer will use this platform to create his or her own products. When the product creator wishes to download the product, the platform charges a fee for this service.
Specifically speaking, let's make a PWA maker. A PWA allows a web app to acquire native apps functionalities and access it through a web browser. This blog post will not cover the process of making a PWA, but it will use this shop idea to serve as a working example.
The process of buying looks something like this:
- The shop user makes a PWA and registers to the shop
- The user clicks on downloads and gets redirected to Stripe checkout
- If the payment is successful, the user is directed back to Ruby on Rails web application
- The user can download the PWA
- The shop user can subscribe to this PWA for deployment and maintenance (also a Stripe checkout)
Basically, one registered user can create multiple products. Maybe it is strange that the user will buy his or her own products. However, what’s even more strange is that the user will subscribe to his or her own product.
Normally, subscriptions are related to users. In this example, a subscription will be related to products, and one user can subscribe to maintenance for multiple products.
Setting up the shop
Before starting to make models, controllers, views (MVC), and other elements, let’s add stripe gem and devise gem to the Gemfile:
gem 'stripe'
gem 'devise'
After adding the gems and running bundle install, a stripe configuration file should be made. It can be placed in config/initializers/stripe.rb, and it looks like this:
Stripe.api_key = ENV["SECRET_KEY"]
One simple line is adding your API key to Stripe. This api key should be kept secret and should not be pushed on Gitlab or Github. The key is stored in ENV variable in config/local_env.yml which looks like this:
SECRET_KEY: "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxx..."
PUBLISHABLE_KEY: "pk_test_yyyyyyyyyyyyyyyyyyyyy..."
Git (if you are using git) should ignore this file, so add it to .gitignore . The Ruby on Rails application should know that there is a new file and that this file should be loaded, so change your config/application.rb accordingly:
…
module PwaMaker
class Application < Rails::Application
...
config.before_configuration do
env_file = File.join(Rails.root, 'config', 'local_env.yml')
YAML.load(File.open(env_file)).each do |key, value|
ENV[key.to_s] = value
end if File.exists?(env_file)
end
...
end
end
If everything is fine, you should be able to see ENV variables in your rails console. You should also be able to make changes to the stripe dashboard (for example creating a new customer):
pwa_maker]$(master) rails c
2.3.1 :001 > ENV["SECRET_KEY"]
=> "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
2.3.1 :002 > Stripe::Customer.create(email: "some.email@gmail.com")
=> #<Stripe::Customer:0x2aff4c04803c id=cus_XXXXXXXXXXXXXXX> JSON: {
"id": "cus_XXXXXXXXXXXXXXX",
"object": "customer",
"address": null,
"balance": 0,
...
Making the models
The shop itself is simple, so let's generate some models. Two models are crucial: user and product. User model will use the devise gem for registration and it looks like this:
# == Schema Information
#
# Table name: users
#
# id :bigint not null, primary key
# email :string default(""), not null
# encrypted_password :string default(""), not null
# reset_password_token :string
# reset_password_sent_at :datetime
# remember_created_at :datetime
# sign_in_count :integer default("0"), not null
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :inet
# last_sign_in_ip :inet
# created_at :datetime not null
# updated_at :datetime not null
# customer_id :string
#
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
default_scope { order(created_at: :desc ) }
has_many :products
after_save :assign_customer_id
def assign_customer_id
if self.customer_id.blank?
customer = Stripe::Customer.create(email: email)
self.update(customer_id: customer.id)
end
end
def products_purchased
self.products.where.not(paid_at: nil)
end
def products_subscribed_on
self.products.where.not(subscribed_at: nil)
end
end
The only extra field here is the customer_id. When the user signs up to the PWA maker, it will automatically create a customer in the Stripe dashboard:
The method for creating the Stripe customer is “assign_customer_id”. This method will send a request to the Stripe API, and the response will contain the “customer_id”. The “customer_id” looks something like this: cus_XXXXXXXXXXXXXXX. If you remember, the same request and response was made earlier in this blog post, but through the rails console.
The second important model is the Product model and it could look like this:
# == Schema Information
# Table name: products
# id :bigint not null, primary key
# title :string
# content :text
# user_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# price :integer
# price_id :string
# product_id :string
# paid_at :datetime
# zip_filename :string
# subscription_price_id :string
# subscription_price :integer
# subscribed_at :datetime
# subscription_id :string
# unsubscribed_at :datetime
#
class Product < ApplicationRecord
validates :title, uniqueness: { case_sensitive: false }
belongs_to :user, optional: true
default_scope { order(created_at: :desc ) }
before_save :set_price
def set_price
self.price = 300
self.subscription_price = 1000
end
after_save :assign_price_id
def assign_price_id
if self.price_id.blank? and self.product_id.blank?
product = Stripe::Product.create(id: self.id, name: self.title, type: "service")
price = Stripe::Price.create(product: product.id, currency: self.currency, unit_amount: self.price, recurring: nil)
subscription_price = Stripe::Price.create(product: product.id, currency: self.currency, unit_amount: self.subscription_price, recurring: {interval: 'month'})
self.update(price_id: price.id, product_id: product.id, subscription_price_id: subscription_price.id)
end
end
def currency
"EUR"
end
def in_main_currency(price)
(price.to_f / 100.0).round(2)
end
def in_main_currency_humanized(price)
sprintf("%.2f #{self.currency}", self.in_main_currency(price) ) #.gsub('.00','')
end
def price_in_main_currency
self.in_main_currency(self.price)
end
def price_humanized
self.in_main_currency_humanized(self.price)
end
def subscription_price_in_main_currency
self.in_main_currency(self.subscription_price)
end
def subscription_price_humanized
self.in_main_currency_humanized(self.subscription_price) + " / month"
end
def is_subscribed
!self.subscribed_at.blank? and self.unsubscribed_at.blank?
end
def is_unsubscribed
self.subscribed_at.blank? and !self.unsubscribed_at.blank?
end
def was_never_subscribed
self.subscribed_at.blank? and self.unsubscribed_at.blank?
end
end
Keep in mind the title content, zip_filename, and user_id ofthe product model contains a lot of Stripe related fields:
price |
The amount a customer has to pay to download the app (in fractional currency) |
price_id |
String by which the price can be found in the Stripe API (price_XXXXXXXXXXXX) |
product_id |
String by which the product can be found in the Stripe API (product_XXXXXXXXXXXX) |
paid_at |
The time the user paid for the product |
subscription_price_id |
String by which the subscription price can be found in the Stripe API dashboard (price_XXXXXXXXXXXX) |
subscription_price |
The amount the customer has to pay for maintaining the app (in fractional currency) |
subscribed_at |
The time the user has paid for the subscription |
unsubscribed_at |
The time the user cancelled the subscription |
subscription_id |
String by which the subscription can be found in the Stripe API (sub_XXXXXXXXXXXX) |
When the user creates the product, the method “assign_price_id” will be called and it will create one product with two prices which can be seen in the Stripe dashboard:
Two prices have two different amounts (always in fractional currency in Ruby on Rails applications) and two different price IDs. The first price is the one-time purchase price, and the second price is the monthly recurring subscription price. In this example, the currency is EUR (Euro) and could be replaced with USD or any other supported currency.
Fields subscribed_at, unsubscribed_at, and paid_at are initially nil and it will be updated when the user successfully finishes the Stripe checkouts. Other methods except the “assign_price_id” methods are helpers that will display the amounts in readable form or check if the user has already paid for something.
Other models could be an Image model that belongs to the Product or an Admin model that could be a person who manages the content on this project. There is no need to cover those models in this blog post. The next task is to create a user interface where the user can create and pay for the products.
User interface
At this point, all functionalities can be accessed through the rails console or through the Stripe dashboard. Users should never have access to any of those two. This is why a custom interface is needed. Firstly, let’s take a look at the confing/routes.rb
Rails.application.routes.draw do
devise_for :users
devise_for :admins
root "pages#home"
resources :products, only: [:create]
%w( 404 422 500 ).each do |code|
get code, :to => "errors#show", :code => code
end
namespace :user do
root "products#index"
resources :products, only: [:show]
resources :charges, only: [:new, :create] do
get "success", to: "charges#success"
get "cancel", to: "charges#cancel"
end
resources :subscriptions, only: [:new, :create] do
get "success", to: "subscriptions#success"
get "cancel", to: "subscriptions#cancel"
post "manage", to: "subscriptions#manage"
get "manage-return", to: "subscriptions#manage_return"
end
resources :unsubscriptions, only: [:create]
end
namespace :admin do
…
end
end
The unregistered user can create a product, and they will then be redirected to the signup form. If the user signs up, the product will be associated with the user (the product will just get the user’s ID).
This approach can have some hidden issues. For example, what will happen with the product if the user doesn’t sign up? Similarly, can the product be assigned to the wrong user? However, this blog post will leave these problems unresolved because they are not related to Stripe.
Create the product
Before explaining the controller, let’s make clear what are actions and methods. Methods are similar to functions in other programming languages, and actions are specific methods in Ruby on Rails controllers. The most simple controller here is the app/controllers/products_controller.rb and it looks like this:
class ProductsController < ApplicationController
def create
@product = Product.new(product_params)
@product.user = current_user if current_user
if @product.save
if current_user
redirect_to user_root_path, notice: t('saved_successfully')
else
cookies[:pwa_id] = { value: encode_id(@product.id), expires: 30.minutes.from_now }
redirect_to new_user_registration_path, notice: "Sign up to download the PWA"
end
else
render :new
end
end
private
def product_params
params.require(:product).permit(:title, :content)
end
end
The “create” action will create the product and associate it with the user if the user is registered. If not, the product ID will be encoded with the custom made “encode_id” method and stored in your cookies. There is no “new” action because the form is embedded on the root page. This form could look something like this:
<%= form_for @product, url: products_path do |f| %>
<%=render partial: 'layouts/error_messages', as: :object, object: @product %>
<div class="form__field">
<%= f.label :title, class: 'label' %>
<%= f.text_field :title, class: 'input' %>
</div>
<div class="form__field">
<%= f.label :content, class: 'label' %>
<%= f.text_area :content, class: 'input' %>
</div>
<%=f.submit 'save', class: 'btn btn--full', data: {disable_with: 'please_wait ...'} %>
<% end %>
After the product is created, the devise gem will handle unregistered users and when the user is registered it will be redirected to the “index” action in app/controllers/user/products_controller.rb:
class User::ProductsController < User::BaseController
before_action :check_for_products
#include CreatePwa
def index
@products = current_user.products
end
def show
@product = Product.find(params[:id])
#make_pwa if @product.zip_filename.blank?
#redirect_to root_path + @product.zip_filename
redirect_to user_products_path
end
private
def check_for_products
if current_user and !cookies[:pwa_id].blank?
product = Product.find( decode_id(cookies[:pwa_id]) )
if product.created_at >= 30.minutes.ago and product.user_id.blank?
current_user.products << product
cookies[:pwa_id] = nil
end
end
end
end
The methods in “CreatePwa” are not included in this blog post, so some steps are commented out. Also, the “check_for_products” method uses “decode_id” which is the counterpart of “encode_id” and could be refactored in the future.
At this point, feel free to redesign your product creation and user registration. Until now, none of the above mentioned steps use the Stripe checkout.
In this example, the app/views/user/products/index.html.erb has four links for each users product that lead to interactions with the Stripe API:
<ul>
<% @products.each do |product| %>
<li>
<%= product.title %> |
<% if product.paid_at.blank? %>
<%= link_to "Pay #{product.price_humanized} to download", new_user_charge_path(charge_id: product.id) %>
<% else %>
<%= link_to "Download", user_product_path(product) %> |
<% if product.was_never_subscribed or product.is_unsubscribed %>
<%= link_to "Subscribe for maintenance", new_user_subscription_path(subscription_id: product.id) %>
<% elsif product.is_subscribed %>
<%= link_to "Cancel subscrption for maintenance", user_unsubscriptions_path(subscription_id: product.product_id), method: :post %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
One-time payment
The first link from the list above is “new_user_charge_path(charge_id: product.id)”, and it leads to the “new” action in app/controllers/user/charges_controller.rb that looks like this:
class User::ChargesController < User::BaseController
before_action :set_product
before_action :redirect_if_paid
layout "stripe/application"
include StripeMethods
def new
end
def create
session = Stripe::Checkout::Session.create({
line_items: [{
price: @product.price_id,
quantity: 1,
}],
mode: 'payment',
customer: current_user.customer_id,
client_reference_id: @product.product_id,
success_url: user_charge_success_url(charge_id: @product.id) + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: user_charge_cancel_url(charge_id: @product.id),
})
redirect_to session.url
end
def success
session = Stripe::Checkout::Session.retrieve(params[:session_id])
customer = Stripe::Customer.retrieve(session.customer)
# is_paid is in concerns/stripe_methods.rb
@product.update(paid_at: Time.now) if is_paid(session, customer)
end
def cancel
end
private
def set_product
@product = current_user.products.find(params["charge_id"])
end
def redirect_if_paid
redirect_to user_root_path unless @product.paid_at.blank?
end
end
This controller uses the app/views/layouts/stripe/application.html.erb as a layout that has no other JavaScript scripts except those which are necessary for Stripe:
<!DOCTYPE html>
<html>
<head>
<title>PWA Maker</title>
<%= stylesheet_link_tag 'stripe', media: 'all' %>
<script src="https://polyfill.io/v3/polyfill.min.js?version=3.52.1&features=fetch"></script>
<script src="https://js.stripe.com/v3/"></script>
<%# two js files must me added to application.rb for precompiling %>
<% if controller_name == "subscriptions" and action_name == "success" %>
<%= javascript_include_tag 'stripe/client.js' %>
<% end %>
<%= javascript_include_tag 'stripe/helpers.js' %>
</head>
<body>
<section>
<%= yield %>
</section>
</body>
</html>
Most of the JavaScript scripts come from Stripe.com, but client.js and helpers.js are in the Ruby on Rails application pipeline. Subscriptions will use client.js that look like this:
// In production, this should check CSRF, and not pass the session ID.
// The customer ID for the portal should be pulled from the
// authenticated user on the server.
document.addEventListener('DOMContentLoaded', async () => {
console.log("client is loaded")
let searchParams = new URLSearchParams(window.location.search);
if (searchParams.has('session_id')) {
const session_id = searchParams.get('session_id');
document.getElementById('session-id').setAttribute('value', session_id);
}
});
Helper.js will just run a simple countdown and redirect the user to the root page:
var secondsLeft = 0;
function runCountdown(id = "js-count") {
var initSec = document.getElementById(id).innerHTML;
secondsLeft = parseInt(initSec) - 1;
setInterval(function() {
document.getElementById("js-count").innerHTML = secondsLeft;
if (secondsLeft>1) secondsLeft--
}, 1000);
};
function runRedirect(url) {
setTimeout(function() {
window.location.href = url
}, (secondsLeft+1)*1000); // 1000 is 1 sec
}
Besides the custom layout, the charges controller includes “is_paid” method from the “StripeMethods” (app/controllers/concerns/stripe_methods.rb) and looks like this:
module StripeMethods
def is_paid(session, customer)
return false unless current_user.customer_id == customer.id
return false unless session.customer == current_user.customer_id
return false unless session.client_reference_id.to_i == @product.id
return false unless session.payment_status == "paid"
return true
end
end
This method should check if all requirements are fulfilled and if a product can be considered paid.
The charges controller is pretty simple now that all the additional stuff is explained. The user clicks on the "Pay 3 EUR to download" link and the “new” method in the charges controller will be called. The template for this action looks like this:
<div class="product">
<img src="https://i.imgur.com/EHyR2nP.png" alt="The cover of Stubborn Attachments" />
<%= image_tag "logo.png" %>
<div class="description">
<h3> <%= @product.title %> </h3>
<h5> <%= @product.price_humanized %> </h5>
</div>
</div>
<%= form_for [:user, @product], url: user_charges_path(charge_id: @product.id), method: :post , :html => {role: 'form', class: ''} do |f| %>
<button type="submit" id="checkout-button">Checkout</button>
<% end %>
The user sees this:
Clicking on the “Checkout” button will make a call to the “create” action in the charges controller. This action creates a Stripe session, which redirects the user to Stripe.com.
The rails application will get this new stripe url from Stripe::Checkout::Session.create action. The user is now on Stripe.com, which indicates that the rails application won’t handle the payments at all.
In this step, the user should enter the required data. In this example, the Ruby on Rails application can’t and shouldn’t save the credit card numbers.
When the user completes the payment, Stripe will redirect the user back to the Ruby on Rails application. It will redirect the user back to “user_charge_success_url” or to “user_charge_cancel_url”, which depends on if the payment was successful or not.
These two URLs are added to the Stripe session in the “create” action in the charges controller. Notice the param “?session_id={CHECKOUT_SESSION_ID}”. “{CHECKOUT_SESSION_ID}” is not a placeholder for some number, It is literally the given string. When the payment is done, Stripe redirects the user to “user_charge_success_url”. It will also send the “params[:session_id]” which will be used to retrieve the session and to convince the “success” action in the charges controller that the payment is valid.
This “success” action will check if the payment is valid with the “is_paid” method, update the product record in the database, and it will render the app/views/user/charges/success.html.erb template, that looks like this:
<p> We appreciate your business! </p>
<p> Redirect to dashboard in <span id="js-count"> 5 </span> or click to <%= link_to "Dashboard", user_root_path %> </p>
<script>
runCountdown();
runRedirect("<%= user_root_path %>");
</script>
Calls on two JavaScript functions in the script tag are unnecessary but can be found in app/assets/javascripts/stripe/helpers.js. The user, or to be more clear, the customer, will see something like this:
Stripe will redirect the user to the “user_charge_cancel_url” if the payment was unsuccessful. The Ruby on Rails application could render a “Try again” template for these situations.
If the product is marked as paid, or, to be precise, that the paid_at field is not nil (it carries now the time and date the product was paid on), the user won’t see the “Pay 3 EUR to download” link in app/views/user/products/index.html.erb anymore,. Instead, they’ll see the “Download” link which can lead to the show action or which can trigger the download process.
Subscription
The third url on the list on app/views/user/products/index.html.erb is “new_user_subscription_path(subscription_id: product.id)” with the text “Subscribe for maintenance”. A click on this link will lead to a “new” action in the subscription’s controller. This controller in the file app/controllers/user/subscriptions_controller.rb looks like this:
class User::SubscriptionsController < User::BaseController
before_action :set_product
before_action :redirect_if_subscribed
layout "stripe/application"
include StripeMethods
def new
end
def create
session = Stripe::Checkout::Session.create({
line_items: [{
# Provide the exact Price ID (e.g. pr_1234) of the product you want to sell
price: @product.subscription_price_id,
quantity: 1,
}],
mode: 'subscription',
customer: current_user.customer_id,
client_reference_id: @product.product_id,
success_url: user_subscription_success_url(subscription_id: @product.id) + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: user_subscription_cancel_url(subscription_id: @product.id),
})
redirect_to session.url
end
def manage
# This will make an error:
# Stripe::InvalidRequestError (You can't create a portal session in test mode until you save your customer portal settings in test mode at https://dashboard.stripe.com/test/settings/billing/portal.)
#
# Allow customer to update the following billing information
checkout_session_id = params['session_id']
checkout_session = Stripe::Checkout::Session.retrieve(checkout_session_id)
session = Stripe::BillingPortal::Session.create({
customer: checkout_session.customer,
return_url: user_subscription_manage_return_url(subscription_id: @product.id)
})
redirect_to(session.url, format: :json)
end
def manage_return
end
def success
session = Stripe::Checkout::Session.retrieve(params[:session_id])
customer = Stripe::Customer.retrieve(session.customer)
# is_paid is in concerns/stripe_methods.rb
@product.update(subscribed_at: Time.now, subscription_id: session.subscription, unsubscribed_at: nil) if is_paid(session, customer)
end
def cancel
# This is not unsubscribe - this is when subscription create fails
end
private
def set_product
@product = current_user.products.find(params["subscription_id"])
end
def redirect_if_subscribed
redirect_to user_root_path unless @product.subscribed_at.blank?
end
end
This controller is similar to the charges controller from the last chapter, but it has two extra actions. Those actions are “manage” and “manage_return”. The first method will lead the user to Stripe.com so the user can change subscription information. The second method, “manage_return”, acts as a place where Stripe.com will redirect the user back to this Ruby on Rails application. It looks like managing subscriptions works only in production and not when the test mode is active.
This checkout works the same as the one-time payment. The action “new” will render the template app/views/user/subscriptions/new.html.erb that could look like this:
<div class="product">
<img src="https://i.imgur.com/EHyR2nP.png" alt="The cover of Stubborn Attachments" />
<%= image_tag "logo.png" %>
<div class="description">
<h3> Maintenance </h3>
<h5> <%= @product.subscription_price_humanized %> </h5>
</div>
</div>
<%= form_for [:user, @product], url: user_subscriptions_path(subscription_id: @product.id), method: :post , :html => {role: 'form', class: ''} do |f| %>
<input type="hidden" name="lookup_key" value="{{PRICE_LOOKUP_KEY}}" />
<button id="checkout-and-portal-button" type="submit">Checkout</button>
<% end %>
The only exception is the “PRICE_LOOKUP_KEY” in a hidden field which is used as a lookup key for retrieving prices dynamically from a static string. This topic is better covered in the Stripe documentation.
However, pay special attention to the “cancel” action. Some developers could make a mistake by trying to use this action to cancel the subscription, but this is wrong. This method is used as a place where Stripe will redirect the user after the subscription has failed for some reason. To cancel a subscription, a new controller should be made.
This unsubscription’s controller can be app/controllers/user/unsubscriptions_controller.rb which looks like this:
class User::UnsubscriptionsController < User::BaseController
before_action :set_product
layout "stripe/application"
def create
if @product.is_subscribed
@product.update(subscribed_at: nil, unsubscribed_at: Time.now)
# Make the subscription least until next payment - customer can activate subscription if the period is not over
# https://stripe.com/docs/billing/subscriptions/cancel?dashboard-or-api=api
Stripe::Subscription.update(@product.subscription_id, {
cancel_at_period_end: true # if the customer subscribes again before the end of period he will be charged again
})
Stripe::Subscription.delete(@product.subscription_id)
end
redirect_to user_root_path, notice: "Unsubscription done!"
end
private
def set_product
@product = current_user.products.find(params["subscription_id"])
end
end
Before the user cancels the subscription, this controller will update the subscription plan with the attribute “cancel_at_period_end”. This will unsubscribe the user at the end of the month or at the end of another period. The Stripe::Subscription could get this attribute at any time, but for the sake of this blog post, let’s not overwhelm other controllers with new information.
The link that triggers the “create” action in the unsubscriptions controller is the fourth link on the list on the app/views/user/products/index.html.erb template. Unsubscriptions controller works without rendering any template.
The Stripe admin has a nice overview of all one-time payments and subscriptions by accessing the Stripe dashboard.
Also, the Stripe admin can find the individual user on the Stripe dashboard and see all the actions the user has made by clicking on the Ruby on Rails application.
Let's summarize
To summarise, Stripe is pretty simple and well-documented. There are a lot of examples in many programming languages that cover the integrations of this payment service. First, the developer needs a working application that offers a product or service. Secondly, the developer should decide how the user will be charged for this product or service. Once that’s accomplished, Stripe can be included in the application.
Each action made by the user will be saved on the Stripe dashboard, and developers should think about what Ruby on Rails should record. Maybe it's better to store more keys provided by the Stripe API than fewer keys, but it depends on the application's task. My advice is not to save credit card numbers and other sensitive data in the application database. Let some external service such as Stripe take care of these for added user security.