Using Stripe Connect To Directly Pay Retailers

Dave Schwantes
tech-at-instacart
Published in
6 min readAug 4, 2016

--

We at Instacart really like it when customers can give us money in exchange for their favorite groceries and because mailing us envelopes of cash would make it hard to support deliveries in as fast as 1 hour we make heavy use of Stripe. One of the coolest Stripe features we utilize is Connect. Stripe Connect allows us to function like a marketplace in certain situations, accepting payments from customers and then directly transferring some or all of that payment to a bank account owned by one of the retailers we partner with.

Because our payment model can be complex, particularly for marketplace type transactions, we’ve really pushed the limits of Stripe Connect and learned a lot about what it is capable of. In this post, I’ll walk you through how we make the most of this powerful tool.

Setting Up A Connected Account

First, in order to use Stripe Connect you will need to register your platform and then connect the retailer Stripe accounts to that platform. I can’t give a better explanation for that than Stripe does in their own documentation, so just take a look at that.

One important aspect of this step is storing the Stripe Connected Account id for the retailer when they connect to your platform. We do this by building a custom endpoint on our own server that takes in a retailer id and the authorization code that Stripe will generate. Then we set the Redirect URI in the Stripe Platform Settings in our Stripe Dashboard to a page so it becomes a part of the flow when retailers connect with us.

Here is what such an endpoint might look like, built in your stripe_connect_controller.rb:

STRIPE_OAUTH_OPTIONS = {
site: 'https://connect.stripe.com',
authorize_url: '/oauth/authorize',
token_url: '/oauth/token'
}
...
def redirect
if set_stripe_connect_account_id(params[:retailer_id], params[:code])
flash[:info] = "Stripe account successfully connected!"
else
flash[:warning] = "Error connecting Stripe Connect account."
end
redirect_to :someplace_useful_for_the_retailer
end
def set_stripe_connect_account_id(retailer_id, code)
response = oauth_call(params[:code])
retailer = Retailer.find(retailer_id)
retailer.stripe_connect_account_id = response.params["stripe_user_id"]
retailer.save
true
end
def oauth_call(code)
client = OAuth2::Client.new(STRIPE_CONNECT_APPLICATION_ID, STRIPE_SECRET_KEY, STRIPE_OAUTH_OPTIONS)
client.auth_code.get_token(code)
end
So now when a retailer sets up a connected account with us, we save their stripe_connect_account_id in our database. This id will be used as a destination parameter to tell Stripe where to send the retailer's portion of the charge.Ok! We have our retailer set up and some money burning a hole in our pocket, let's buy things!Auth and CaptureNow when we create a Stripe auth charge we need to specify if we will be transferring some money to a connected account. To do this we'll be setting the destination field on the Stripe charge when we create the auth. This field can't be added or changed once the charge is created so you need to always know if you'll be using Stripe Connect at the time of creation.Here's an example:def create_order_auth_charge(order)
charge_params = {
amount: order.amount_cents, # 10000 ($100)
customer: order.customer.stripe_customer_id, # cus_1234
capture: false
}
if order.needs_direct_payment?
charge_params[:destination] = order.retailer.stripe_connect_account_id # acct_abc123
end
Stripe::Charge.create(charge_params) # return a Stripe::Charge object, id: ch_1337
end
This method will create an auth charge that will transfer 100% of the amount to the account specified by the destination field. The platform account (Instacart) pays all Stripe fees in this transaction, so if $100 is captured, $100 will go to the destination account.Let's say we only want to give $80 of the $100 we authed to the retailer. This is done by setting an application fee on the Stripe capture:charge = Stripe::Charge.retreive("ch_1337")
charge.capture(application_fee: 20_00)
If we want to just create a charge that is captured right away, we can include the application fee when the charge is created:def create_order_charge(order)
charge_params = {
amount: order.amount_cents, # 100_00 ($100)
customer: order.customer.stripe_customer_id, # cus_1234
capture: true
}
if order.needs_direct_payment?
charge_params[:destination] = order.retailer.stripe_connect_account_id # acct_abc123
charge_params[:application_fee] = order.amount_cents - order.amount_to_retailer_cents # 100_00 - 80_00
end
Stripe::Charge.create(charge_params) # return a Stripe::Charge object, ch_3141
end
The mechanisms behind Stripe Connect are Stripe transfers and fees between accounts that happen when the charge is captured. Here is what exactly what is happening for all involved parties:The customer pays $100 which passes through Instacart's platform account and is given to the retailer. Then a $20 fee is taken back from the retailer and given to Instacart. The final balance is $20 for Instacart (minus Stripe processing fees) and $80 for the retailer.
Chart 1@2x
RefundsRefunds with Stripe Connect are a bit more complicated than a normal refund because we're now returning money from multiple bank accounts and we care which accounts we refund that money from.Full RefundUnfortunately, doing a full refund of a Stripe Connect charge is not as simple as it seems like it should be.Let's take our first charge where we captured $100 from the customer, sent $80 to the retailer, and kept $20 for ourselves:charge = Stripe::Charge.retreive("ch_1337")charge.refund # this seems like what we should do, but NO! DO NOT DO THIS!The end result of this call is $100 is returned to the customer by taking a $100 out of the platform (Instacart's) account, with the retailer keeping the $80 we sent them earlier. That means we're out $80 and someone from accounting is mad at me.What we need to do is first refund the fee we took from the retailer, then take back the whole transfer from the retailer, THEN return the full $100 to the customer.charge.refund(reverse_transfer: true, refund_application_fee: true) # DO THIS FOR FULL REFUNDS!
Chart 2@2x
Refunds from retailer portionRefunding a portion of the charge that was sent to the retailer is also a bit complicated. Let's say we have the same charge as before ( $100 from the customer, $80 to the retailer, $20 kept by Instacart). From this charge we want to refund $10 from the portion of the charge that was already sent to the retailer.To do this we'll want to manually reverse part of the transfer that was created as a part of this charge. As mentioned earlier, the mechanism behind Stripe Connect is transfers. We have a record of those transfers associated with the charge, so we can find them and work with them:charge.refund(amount: 10_00) # this refunds $10 to the customer from the platform account
transfer = Stripe::Transfer.retrieve(charge.transfer)
# this returns $10 to the platform account from the retailer account to cover the refund
transfer.reversals.create(amount: 10_00)
We can't use the reverse_transfer parameter in the refund method here because if we do that Stripe will try to reverse a proportional amount of the transfer, relative to the amount refunded. In the above example using reverse_transfer: true on the refund (and not doing a manual transfer reversal) would refund $8 from the retailer and $2 from the host.
Chart 3@2x
Refunds From Our PortionNow, we finally get to something that just works the way you'd expect it to. Refunding money that we want to come from our account works like a normal Stripe refund:charge.refund(amount: 10_00) # this refunds $10 to the customer from the platform accountChargebacks And DisputesIt is also worth noting that when a chargeback occurs, the amount is refunded from the platform account.Stripe Connect is a very powerful tool and we get a lot of value from it. Hopefully this provides some deeper understanding of how it can be used in real-world situations.

--

--