Stripe Connect onboarding with Ruby on Rails

Stripe Connect provides a suite of tools and APIs that allow you to create, manage, and scale your platform or marketplace and facilitate payments and payouts for your users. If you’re new to Stripe Connect, take a look at the first few articles in this series. In this article you’ll learn how to integrate Stripe Connect Onboarding with Ruby on Rails, so that you can start facilitating money movement for your users.

We'll focus on an email newsletter platform use case, where readers support their favorite authors by paying a monthly fee to receive periodic emails. You’ll learn how to create Stripe Accounts and collect business details using Stripe Connect hosted onboarding. We’ll also cover setting up the Rails environment and some best practices for smooth integration.

Let's get started!

Creating a new Rails application

We’ll kick things off by breaking ground on a new Rails application that uses Tailwind CSS for styles and Postgres for the database. -T means skip adding the default testing infrastructure and --main sets the git branch name.

rails new newsletter-platform -c tailwind -j esbuild -d postgresql -T --main

We’ll store each newsletter issue in the database so that authors can direct users to their back catalog if they’d like to read past issues. Before sending a newsletter issue to a reader, we’ll check to make sure readers have an active payment subscription.

Let’s start by setting up these database models.

Database setup

To set up the necessary database models for this newsletter platform, we'll create the main models: User, Newsletter, NewsletterIssue, and Subscription. The User model represents the author, the Newsletter model corresponds to each collection of issues, the NewsletterIssue model represents each monthly edition of the newsletter, and the Subscription model links the readers to the newsletters they have active payment subscriptions for. Let's begin by generating these models.

Generating the User model:

The User model is used for authentication. Both readers and authors are represented in the database as Users.

We store the stripe_customer_id for readers. We’ll use the Stripe API to create customer objects for all users so that we can keep track of all subscriptions and invoices that are related to a given reader.

We store the stripe_account_id for authors. This represents the ID of the author’s Stripe Account and enables us to route payments from readers to the author. We also store charges_enabled and payouts_enabled flags to know when the account is fully onboarded and can successfully receive money.

rails generate model User name:string email:string stripe_customer_id:string stripe_account_id:string charges_enabled:boolean payouts_enabled:boolean

Generating the Newsletter model:

Newsletters have a foreign key relationship back to the ID of the author in the Users table.

rails generate model Newsletter user:references title:string

Generating the Newsletter Issue model:

Newsletter Issues are related to the newsletter they are about and to start, we’ll simple with a title and block of text for content. The published_at datetime enables us to schedule issues to be published in the future.

rails generate model NewsletterIssue newsletter:references subject:string content:text published_at:datetime

Generating the Subscription model:

When a reader subscribes to a newsletter for the first time, we’ll send them through a payment flow using Stripe Checkout to collect their payment details and start a Stripe Subscription that collects recurring payments. We’ll store that stripe_subscription_id in the database so that we can check the Stripe API to know whether payment is active.

rails generate model Subscription user:references newsletter:references stripe_subscription_id:string status:string

Run the database migrations:

rails db:migrate

Now, let's set up the relationships between these models:

In app/models/user.rb:

class User < ApplicationRecord
  has_many :newsletters
  has_many :subscriptions
end

In app/models/newsletter.rb:

class Newsletter < ApplicationRecord
  belongs_to :user
  has_many :newsletter_issues
end

In app/models/newsletter_issue.rb:

class NewsletterIssue < ApplicationRecord
  belongs_to :newsletter
end

In app/models/subscription.rb:

class Subscription < ApplicationRecord
  belongs_to :user
  belongs_to :newsletter
end

With these models in place, we can now represent authors, newsletters and their issues, and reader subscriptions in our database. In the next steps, we'll implement the Stripe Connect Onboarding logic.

Set up Stripe

Use the stripe-ruby SDK to interact with the Stripe API.

bundle add stripe

Retrieve API keys from dashboard.stripe.com and set those into our Rails credentials.

EDITOR=vi rails credentials:edit

Insert the keys like so:

stripe:
  secret_key: sk_test_51EceeUCZ6qs...
  publishable_key: pk_test_vAZ3gh1Lc...

Add an initializer to config/initializers/stripe.rb in order to set the platform level API key. Note that each author’s Stripe account will have its own API keys, but with Stripe Connect, we never need the connected account’s keys. Instead we authenticate requests for connected accounts with the combination of our platform level API key and the connected accounts’ ID. See here for more details.

Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)

Now we’re ready to start making API calls to Stripe from Ruby. Before we start using the Stripe API, let’s get authentication up and running.

Set up authentication

For this use-case, users need a way to authenticate into the application. Since we’re using Ruby on Rails, we’ll use devise authentication for authors.

We’ll install the devise gem, run the install scripts and generate the routes and migration for Authors to be database authenticatable.

bundle add devise
rails generate devise:install
rails g devise:views
rails g devise Author

Again, we migrate the database.

rails db:migrate

Running bin/dev fires up the server so we can test our registration flow at localhost:3000/authors/sign_up.

We’re presented this unstyled login view that we’ll clean up later.

Now that users can register for the application, we’ll want to onboard them to Stripe Connect so that we can start interacting with the Stripe API on their behalf.

Set up Connect onboarding

Connect supports 3 different account types: Standard, Express, and Custom. Since authors might not have experience handling refunds, and chargebacks in this use case, it makes sense to provide them an integration where they have access to the simpler Stripe dashboard with a Express-type connected account. Learn more about the tradeoffs between different account types here. Side note: we’re hoping to remove the concept of an account type so stay tuned for a less confusing approach to differentiating Connect functionality for your users.

For onboarding, we’ll have a page where we’ll either show the account details for the connected account, or a button for the user to create a new account and go through the onboarding process. These functions will be handled by a new StripeAccountsController.

rails g controller StripeAccounts show

We’ll add singular resource routes for /stripe_account by updating config/routes.rb.

Rails.application.routes.draw do
  resource :stripe_account
  devise_for :users
end

Next, we’ll add the before action macro from devise that requires an authenticated author to access these routes.

class StripeAccountsController < ApplicationController
  before_action :authenticate_author!
end

The view for the show route is very simple for now.

<% if current_user.stripe_account_id.present? %>
  <%= current_user.stripe_account.to_json %>
<% else %>
  <p>No Stripe account found</p>
  <%= button_to "Create a Stripe Account", stripe_account_path, method: :post, data: { turbo: false } %>
<% end %>

When a user clicks on “Create a Stripe Account”, we’ll first make an API call to Stripe to create a new Express account, then we’ll update the database with the account’s ID and finally redirect through the account onboarding flow with an Account Link.

The goal here is to minimize the amount of information the author needs to re-enter. For instance, we already have the author’s email address, so we can prefill that at the account and individual level. We’ll also assume that all authors are individuals rather than businesses. We can prefill the business profile’s mcc (merchant category code) as digital goods so that each author isn’t required to dig through the list of service types to find digital goods.

  def create
    account = Stripe::Account.create(
      type: 'standard',
      email: current_user.email,
      business_type: 'individual',
      business_profile: {
        mcc: '5818',
      },      
      individual: {
        email: current_user.email,
      },
      metadata: {
        author_id: current_user.id,
      }
    )
    current_user.update(stripe_account_id: account.id)

    account_link = Stripe::AccountLink.create(
      account: account.id,
      refresh_url: stripe_account_url,
      return_url: stripe_account_url,
      type: 'account_onboarding'
    )

    redirect_to account_link.url, status: :see_other, allow_other_host: true
  end

When events related to Stripe accounts happen in Stripe, the application can be notified using webhooks. We need to listen for the account.updated webhook event type so that we know when an account has successfully completed onboarding.

Set up webhooks

We need a controller to handle the incoming POST requests from Stripe.

rails g controller Webhooks

We’ll add a simple route of /webhooks for handling POST requests.

resources :webhooks, only: [:create]

Since the requests come from Stripe, we cannot verify any CSRF token, so we skip that check at the top of the controller.

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

Then we’ll add a create method for handling post requests from Stripe to deserialize the event body and switch based on the type of event notification.

  def create
    payload = request.body.read
    event = nil

    begin
      event = Stripe::Event.construct_from(
        JSON.parse(payload, symbolize_names: true)
      )
    rescue JSON::ParserError => e
      # Invalid payload
      puts "⚠️  Webhook error while parsing basic request. #{e.message})"
      render json: { message: 'failed' }, status: 400
      return
    end

    case event.type
    when 'account.updated'
      account = event.data.object # contains a Stripe::Account
      # TODO: Handle account updates
    else
      puts "Unhandled event type: #{event.type}"
    end

    render json: { message: 'success' }
  end

Each time we receive the account.updated event we want to update our local Author’s flags for whether charges and payouts are enabled.

    when 'account.updated'
      account = event.data.object # contains a Stripe::Account
      author = User.find_by(stripe_account_id: account.id)
      author.update(
        charges_enabled: account.charges_enabled,
        payouts_enabled: account.payouts_enabled
      )

To build and test webhooks locally, we’ll use the Stripe CLI. The listen command enables us to forward both account and connect webhook events. Account a.k.a. direct events are those events that fire on our account, connect events are those that happen on connected accounts.

Start the listener with:

stripe listen --forward-to localhost:3000/webhooks --forward-connect-to localhost:3000/webhooks

As a shortcut with Rails, I add a new process to Procfile.dev so that this starts each time we run bin/dev. My Procfile.dev looks like this:

web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch
stripe: stripe listen --forward-to localhost:3000/webhooks --forward-connect-to localhost:3000/webhooks

Now we can go through the onboarding flow and enter test details. Use the magic test strings from this doc to ensure a verified test account. Note you’ll also need to verify your email in order to activate the account, so use a real email address that you have access to when creating the new connect account.

If all went to plan, you see the JSON for the Stripe Account in the browser, your Author has a Stripe Account ID, charges enabled is true, and payouts enabled is true in the database.