- Published on
Building a Marketplace using Stripe Connect (with Examples)
- Authors
- Name
- Adam Stevenson
- @adamjstevenson
Over the past few years, I've spent a significant amount of my time working with Stripe Connect. Connect is a set of APIs that makes it relatively trivial to build payment platforms and marketplaces, a process which has traditionally been just an unbelievably enormous (in bold AND italics) pain in the ass. Why was it such a pain in the ass? Primarily because (1) this tended to require a gross, dispirate set of services to handle the different components of handling payments in, payments out, reconciliation, etc. and (2) it turns out there's a ton of regulation around moving money in ways that seem both completely logical to normal human beings and completely insane and worrisome to regulators, banks, and other people who wear ties. In this post I'll talk about how Stripe makes this easy, things to consider when building a platform with Connect, and introduce a few examples you can look at to help build your own platform.
How building a marketplace works
In the past, I built some of my own marketplace apps and primarily worked with a large, popular payment service that I'll leave unnamed for the purpose of this post. To help think through some considerations in the context of a real marketplace, let me introduce you to Notesurf. Notesurf was a marketplace I built for students to buy and sell college notes and study guides. It was a pretty typical platform service where sellers (college students) posted content and buyers (...also college students) purchased that content. The buyers got the notes they purchased instantly, the sellers got paid a week after the purchase was made, and the platform grew a few bucks stronger thanks to that sweet, sweet commission on each sale.
As it relates to payments, building something like Notesurf requires:
- A way for buyers on the site to purchase notes. Easy enough. 😁
- A method to attribute sales of the notes to particular sellers. I got this. 😊
- Some way to take that sweet platform commission on the sale of each item. Sounds a little trickier, but okay. 😏
- Handling payouts for each seller. This sounds a little more involved. 😕
- Managing tax reporting and creating/distributing 1099s to each seller. Come again? 😦
- Adhering to KYC regulations to ensure you're not enabling money laundering or funding terrorism. OMG WHAT?? 😱
- Lots of other horrific things with payments. 😩
You learn pretty quickly that building platform payments isn't as easy as accepting funds from party A and giving some to party B. On demand ridesharing services like Lyft feel like magic not just because a car shows up at the click of a button, but because there's no discussion of fares, no need to carry cash, and no concerning yourself with the exchange of money at all. But behind the scenes there's a lot going on to store and charge your payment method, verify the driver's identity, make sure they get paid, manage tax reporting, etc. And that's where a service like Stripe Connect comes in and abstracts away all of these terrible things.
Using Stripe Connect to build a marketplace
Connect gives you a lot of flexibility in how you create your platform. The Connect docs do a great job of outlining different implementation options in detail, but at a high level there are 3 different approaches. These are outlined below, along with some examples of using Stripe Connect with each different implementation.
Standard accounts
Standard accounts are the easiest place to get started if you want to build a platform and require the least amount of integration effort. You probably want to choose this option if:
- You want Stripe to have a direct relationship with your user.
- You want Stripe to manage identity verification, reporting, and communication about things like disputes.
- You don't want to build out your own dashboard, notifications, etc.
- The user connected to your platform should ultimately own fraud and dispute liability.
With Standard accounts, your user connects via OAuth to your platform. This generally means you provide a button that your user clicks to Connect to Stripe directly and provide your platform with access to their Stripe account.
nce your user is connected, your platform will primarily authenticate as the connected account when you need to do payments things. For all Connect integrations, you can use the Stripe-Account header. This is nice because it means all you need to store is a relationship between your platform user and the Stripe account ID (instead of scary secret keys). The relationship in your own model would look roughly like this:
user_id | username | stripe_account |
---|---|---|
1203413 | abe_froman | acct_j1cXUEAksEpalsjc3 |
1203414 | c_frye | acct_1BF8VRBAxruRdpTq |
When working with Standard Connect, your user has their own Stripe dashboard that they can use to view payments, issue refunds, submit dispute evidence, create reports, configure their payout settings, and a lot more. As you can imagine, this saves a lot of time since you don't have to build all of this stuff for your platform. Though you'll still have a lot of control, the APIs you interact with most often when using a Standard Connect platform are listed below with some examples.
Charges example
The Charges API is your bread and butter and how your user (and likely your platform) will make money. In the context of Notesurf, imagine a student wants to sell $100 worth of notes (they're amazing notes 💰) and my platform takes a 10% fee. Here's an example in Ruby of creating a $100 charge directly on the account while taking a $10 commission for your platform:
# Create a charge for $100 and take a $10 commission for the platform
charge = Stripe::Charge.create(
{
amount: 10000, # Charge amount in cents
currency: "usd", # The currency of the charge
source: "src_1BF7yx2jy8PDLWD6TFmguGn0", # A source token representing a Visa card
application_fee: 1000 # The commission your platform is taking
},
{
stripe_account: "acct_j1cXUEAksEpalsjc3Xl3" # ID of the Stripe account
}
)
Refunds example
Your end user will be able to issue their own refunds directly through the Stripe dashboard, but your platform can also issue refunds on their behalf if necessary with the Refunds API. Here's what that looks like:
# Refund a charge
refund = Stripe::Refund.create(
{
charge: "ch_1BAkyv2jy8PDLWD6E4t2XxPX", # The ID of the charge to refund
refund_application_fee: true # Optionally give your platform commission back
},
{
stripe_account: "acct_j1cXUEAksEpalsjc3Xl3" # The ID of the Stripe account
}
)
Customers example
You might also interact with the Customers API to store payment details for later use. This is helpful for enabling repeated purchases — either as part of a recurring subscription or if you want to create a one-off charge for a customer without requiring them to enter a payment method again. For Notesurf, this might mean a student storing their payment info to enable one-click purchasing of notes later.
In this example we're creating a customer and storing their payment details on the connected account. You can also store customers on your platform and share them with the connected account using what Stripe calls Shared Customers, which is useful for sharing customer payment details between connected accounts.
# Create a customer on a connected account
customer = Stripe::Customer.create(
{
description: "Customer for jayden.davis@example.com",
source: "src_1BF7yx2jy8PDLWD6TFmguGn0", # The payment source
},
{
stripe_account: "acct_j1cXUEAksEpalsjc3Xl3" # The ID of the Stripe account
}
)
Subscriptions example
If you want to subscribe the purchaser to a subscription, you can do so with the Subscriptions API. This subscription is created directly on the connected Standard Account. In this example, we'll create a subscription for $29 a month. Your platform takes a 10% application_fee_percent
as commission for enabling the subscription and the connected account gets the remainder.
# Create a subscription and take a 10% commission
subscription = Stripe::Subscription.create(
{
customer: "cus_BcNDyG0NuHZn4i", # The ID of the customer on the connected account
items: [
{
plan: "29_monthly" # The ID of the plan
}
],
application_fee_percent: 10
},
{
stripe_account: "acct_j1cXUEAksEpalsjc3Xl3" # The ID of the Stripe account
}
)
There are a number of other APIs you can use with Standard Connect integrations as well. Note that some settings — like payout schedules — can't be controlled by your platform when using Standard Connect.
Standard Connect example applications
A couple different examples of Stripe Standard Connect apps are listed below:
- OAuth examples from Stripe's docs (Various languages)
- Rails + Stripe Connect Example Application (Ruby)
Custom accounts
Custom accounts are the most flexible option for using Stripe Connect, but this type of integration also involves more development work since your platform controls the entire experience. Custom accounts are particularly popular with platforms with complex funds flows or those that want to fully whitelabel and manage the end user payments experience. A platform generally choses to build on Stripe Custom accounts when:
- You want to fully control the onboarding experience and own communications with your platform users.
- Your platform uses a custom dashboard and controls the flow of funds completely. End users only interact with Stripe through it's APIs (which the platform controls).
- You prefer to own liability for payments. In this relationship, end users may not even be aware of Stripe's existence.
Accounts examples (handling onboarding with Connect Custom accounts)
Using Custom accounts involves creating and updating Stripe accounts via the Account API. This is nice because your platform ultimately decides on how to handle onboarding and users can start accepting payments very quickly (for most payment methods, even before a name or anything else is provided by the user). In this example, we'll create a Stripe account for our platform user (say a student selling class notes), then update the information that's required as it's needed.
After signing up for an account, we'll provide a form like the one below to collect information for our user to create their Stripe account.
When the user submits the form, this information will be sent to Stripe to create an account using the Stripe Accounts API creation method:
# Creates a Stripe account with some minimal account information submitted from a form
stripe_account = Stripe::Account.create(
type: "custom",
legal_entity: {
first_name: account_params[:first_name].capitalize,
last_name: account_params[:last_name].capitalize,
type: account_params[:account_type],
dob: {
day: account_params[:dob_day],
month: account_params[:dob_month],
year: account_params[:dob_year]
}
},
tos_acceptance: {
date: Time.now.to_i,
ip: request.remote_ip
}
)
This will return an Account object with a lot of properties, but you're most interested in the account ID that's provided.
#<Stripe::Account:0x3fd4eb18e3f4 id=acct_1BF8VRBAxruRdpTq> JSON: {
"id": "acct_1BF8VRBAxruRdpTq",
"object": "account",
"business_logo": null,
"business_name": null,
...
Just as with Standard accounts, you'll want to save the account ID that's returned via the API and create an association in your database with your authenticated user. By doing so, you can control their Stripe account later.
This type of incremental onboarding is nice because you're able to get an account spun up immediately. As more payments are processed, Stripe will incrementally request additional details about this user by firing an account.updated
event. The fields_needed on that event show what information is needed (if any).
You can automate this process by catching this event with a webhook and sending an email to the accountholder to let them know more info is needed to continue processing payments or access their earnings. When they login to your application, your application retrieves the account and inspects the fields_needed
array to determine what info is needed at this stage. Let's say the following fields are needed:
["legal_entity.address.city",
"legal_entity.address.line1",
"legal_entity.address.postal_code",
"legal_entity.address.state",
"legal_entity.ssn_last_4"]
Using the data you retrieve on the Account object, you would dynamically build a form to collect the fields listed there. Here's a very simple example:
# Retrieve the account
account = Stripe::Account.retrieve("acct_1BF8VRBAxruRdpTq")
# Grab the fields needed
fields_needed = account.verification.fields_needed
# ...later in your form, display an account update form based on the fields needed for verification
<% if fields_needed.include?('legal_entity.business_name') %>
<div>
<%= f.label :business_name, "Business name (as it appears to the IRS)" %>
<%= f.text_field :business_name %>
</div>
<% end %>
<% if fields_needed.include?('legal_entity.business_tax_id') %>
<div>
<%= f.label :business_tax_id, "Business tax ID/EIN" %>
<%= f.text_field :business_tax_id%>
</div>
<% end %>
...
The end result is a nice form that collects information from your users as you need it:
Charges example
In this example we'll create destination charges and separate charges and transfers. I'll skip covering the different ways to create charges with Connect, but there's an excellent breakdown in Stripe's docs here.
Once your user is signed up and you have a Stripe account ID for them, you'll be able to create charges on their behalf. In this example, we're creating a charge for $10 and taking a commission of $1 on the charge.
charge = Stripe::Charge.create({
amount: 1000, # The total amount of the charge
currency: "usd",
source: "src_1BF7yx2jy8PDLWD6TFmguGn0", # A token representing the customer's payment details
destination: {
amount: 900, # The amount the connected user gets
account: "acct_1BF8VRBAxruRdpTq", # The ID of the connected user
}
})
It's important to point out that in this scenario, the platform pays the processing fees. So although your platform takes $1, your net platform earnings will be $1 less the processing fees.
If you want to get even more magical, you can further split payments using separate charges and transfers. Imagine instead that you want to create a charge for $10, keep $1 for yourself, and split the remainder between two different sellers. You can accomplish this by using Stripe's newer functionality like so.
# Create a charge for $10
charge = Stripe::Charge.create({
amount: 1000,
currency: "usd",
source: "src_1BF7yx2jy8PDLWD6TFmguGn0",
transfer_group: "notes_328324"
})
# Send $4 to one account
transfer = Stripe::Transfer.create({
amount: 400,
currency: "usd",
destination: "acct_j1cXUEAksEpalsjc3",
transfer_group: "notes_328324"
})
# Sent $5 to another
transfer = Stripe::Transfer.create({
amount: 500,
currency: "usd",
destination: "acct_1BF8VRBAxruRdpTq",
transfer_group: "notes_328324"
})
Finally, you want to display these charges to your user in a dashboard so they can see their earnings. For large platforms, you'll probably want to use webhooks to collect and store transactional detail in your own model since this will be much faster to query and doesn't rely on a connection to Stripe. But it's also possible to retrieve this from Stripe directly either using the balance history endpoint or the list charges endpoint. For this examample, we'll use the later.
After our sellers have received payments, we'll list them out in a dashboard so they they can view them like so:
Behind the scenes we're retrieving the charges for the connected account by authenticating as them using the Stripe-Account header:
# Last 100 charges
payments = Stripe::Charge.list(
{
limit: 100, # The number of charges to retrieve (between 1 and 100)
expand: ['data.source_transfer', 'data.application_fee'] # Expand other objects for additional detail
},
{ stripe_account: current_user.stripe_account } # The Stripe ID of the user viewing the dashboard
)
Then we’re iterating through the charges to display them. Here’s an example of what that might look like in a view for a Rails app:
<div class="row">
<div class="col-md-12">
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Payment ID</th>
<th>Amount</th>
<th>Net</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @payments.each do |payment| %>
<tr>
<td><%= link_to payment.source_transfer.source_transaction, charge_path(payment.source_transfer.source_transaction) %></td>
<td><%= number_to_currency(payment.amount/100) %></td>
<td><%= format_amount(payment.amount - payment.application_fee.amount) %></td>
<td><%= format_date(payment.created) %></td>
<td class="text-center">
<% if payment.refunded %>
<span class="text-danger">
<span class="fa fa-undo"></span> Refunded
</span>
<% else %>
<%= link_to "Refund", charge_path(payment.source_transfer.source_transaction), method: :delete %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
Balance Transactions and Payouts example
Stripe provides automatic payouts, which greatly simplifies the disbursement process for you as the platform. Though manual payouts are available, using automatic payouts means you offload this typically painful process to Stripe and let them handle the details. Stripe also creates a nice ledger for transactions via the Balance API that you can tap into any time.
Another thing you'll definitely want to display for connected accounts is their payout detail. This relates the charges coming in to the payments made to their bank accounts. We can do this easily with the balance history endpoint and the ID of the payout. Again, imagine you want to show an overview of payouts in a dashboard for your users:
Again, you can get a nice list directly from Stripe's list payouts endpoint for this view like so:
# Last 100 payouts from the custom account to their bank account
@payouts = Stripe::Payout.list(
{
limit: 100,
expand: ['data.destination'] # Get some extra detail about the bank account
},
{ stripe_account: current_user.stripe_account } # Again, authenticating with the ID of the connected account
)
When a user selects a particular payout, they see the detail on the transactions that make up that payout:
By using the list balance transactions method and passing in the ID of the payout in the payout
parameter, a list of the transactions making up that payout are returned:
# Get the balance transactions from the payout for the payout transactions view
@txns = Stripe::BalanceTransaction.list(
{
payout: params[:id], # The ID of the payout you want transactions for
expand: ['data.source.source_transfer'], # Expand the source_transfer for extra detail
limit: 100
},
{ stripe_account: current_user.stripe_account }
)
Other API examples
If you're building a Custom Connect integration, you'll interact with many of Stripe's APIs. I won't cover all of them here, but this should be a good start on understanding what's required and how your users will interact with Stripe through your platform. You can find more detail in Stripe's API reference.
Custom Connect example applications
There's an example application I put together in Rails that you can use in test mode here. Create a campaign and charges to see the dashboard in action and check the code for this demo on Github to see what's happening behind the scenes.
Express accounts
Express accounts are Stripe's newest option and provide a lot of the benefits of Standard Connect while also allowing the platforms more control of the branding and funds flows of the connected account. This method also uses OAuth to connect your user to your platform and there are some requirements for using Express accounts (like having an SMS-enabled phone number and presence in the US) that you should be aware of.
So why would you choose Express accounts?
- You want to leave the onboarding components (identity verification) to Stripe.
- You want to get up and running quickly and are okay with Stripe managing communications with the user.
- Your platform only needs a lightweight dashboard and is okay with ultimately owning fraud and dispute liability.
API examples
I won't cover separate API examples since you'll use many of the same APIs that you do with Custom accounts. That said, you won't need to be as concerned with the onboarding components (primarily the Accounts API).
Express Connect example application
There's an excellent Express demo created in Node available at Rocketrides.io. You can also find the code for this demo on Github.
Other thoughts
The intent of this post is primarily to highlight the things to consider when building a payments platform. Although there's a lot to consider and building a platform is relatively involved, this process sucks considerably less than it did in the past. Connect is really good at this and exists specifically to reduce your overhead and abstract away a lot of this complexity so you can focus on the really hard part (finding both buyers and sellers to use your service).
I have a goal to build a more full-featured Stripe Connect tutorial, so feel free to check back or bug me on Twitter if this would be helpful!