Integrating Stripe (Connect)
[Written: Mid 2021]
I've used Stripe for about six years now. Hit me up if you have questions.

Products I've used

First few years (2015-2020)

Checkout.js – v1
  • One-time charges
  • Subscriptions
  • Save card for future use
  • Create coupons via API, redeem coupons at checkout
Stripe Elements, with a token and ChargesAPI
  • One-time charges
  • Subscriptions
  • Saving card for future use
Stripe Elements, with PaymentIntent
  • Create one-time charges
  • Create subscriptions
  • Saving card for future use
  • Publishers (partially) refunding payments or canceling a subscription in custom UI (using API)
  • Customers deleting card on file, viewing subscription details next invoice, in custom UI
Handling webhooks
  • e.g., PaymentIntent succeeded; new subscription payment; subscription payment failed; payment method updated (hence re-try an overdue subscription); payout succeeded; etc. Any actions carried out via the Stripe dashboard are reflected in any relevant local records (e.g., payment amount, amount_refunded, amount_for_recipient).
  • Python API. Custom checkout built using Django and jQuery.
 

Recently (late 2020 and 2021)

  • Stripe Connect – for Awesound's audio marketplace (purchases and subscriptions)
  • Stripe-hosted Checkout
  • Stripe on Next.js

Stripe Connect

⚠️
The docs are great, but some decisions you make here are non-trivial, or at least non-reversible. You want to think through exactly how you're going to pay out your sellers, what currencies they'll charge in, and where in the world they are:
Where your platform Stripe account is
  • There might be a feature available only to US accounts. UK accounts can easily send money to EU bank accounts; US platform accounts can't send money "overseas" unless the recipient account is "recipient".
Connect Account type
  • Standard
  • Express
  • Custom
Payment type
  • Direct charges
  • Destination
  • Separate charges and transfers
on_behalf_of
  • Only relevant for Destination charges.
  • Direct charges are always made by the Connected account. Separate charges and transfers are always made by our (platform) account. Destination charges are made by our (platform) account by default, but we can set the Connect account as the settlement merchant by setting on_behalf_of.
  • Major advantage: avoids currency conversion fees (e.g., € purchase to € author, don't convert to USD and abck to €).
    • Concretely, a UK seller, selling an audiobook in GBP can have predictable earnings in GBP, without converting GBP to USD and back to GBP. (Awesound's platform account is in USA.)
  • Minor advantage: might reduce decline rates. Minor advantage/disadvantage: publisher's phone number appears on credit card statement (can we change this?)
  • Service agreement type: "full" or "recipient"

Watch out if you want to support international sellers with split payments

You opt for Separate Charges and Transfers so that sellers can be affiliates too, and split earnings for each payment. International sellers start signing up.
See cases 1-3 below. The conundrum is:
  • Destination Charges and Direct Charges only allow you to specify one payment recipient.
  • You can pay additional parties (e.g., later pay the person who actually does the work, or give 5% to an affiliate) by using stripe.Transfer.create() But! This only works if the connected account is in your region, or the account has a recipient service agreement.
  • But! If an account has a recipient service agreement, they can never have card_payments capability, so when they sell a product in EUR, it'll be converted to USD.
👉
Concretely, if you're a US Platform Account and each customer payment might need to be split between two (or more) recipients, both of whom are non-US: you have to decide which pain to embrace
  1. Eat the currency conversion fee, and have all non-US accounts as recipient for custom Transfers any amount, any time you like.
  1. Have international sellers create two Connect accounts
      • one full so you can use Direct Charges or on_behalf_of(with Destination Charges) to avoid currency conversion fees (when they're the "main recipient" aka "seller")
      • one recipient to receive their delayed/affiliate payments (when they're the "affiliate")
  1. Choose to not support split payments for international sellers, or find some other way to pay them (e.g., not using Stripe… Dare I mention… PayPal? Manual bank transfers?)
      • This is the sacrifice I made. I'll worry about multi-recipient split payments later. For now, I just want to minimize payment processing fees, so I can pay each author more money.

Some "gotchas" to be aware of when setting up Connect for international sellers

Cases to watch out for before you start

Case 1. An international seller becomes an affiliate, but you can't pay them

👉
Context: You, a US platform, have an international seller sign up as an Express Connected Account with service agreement full. They are mildly successful as a seller, but super successful as an affiliate.
By "seller", I mean: the main service provider who should receive most of the customer payment. By "affiliate", I mean: someone on your platform who's earning ad-hoc referral fees, or supposed to be getting a share of a customer payment, where the payment is split between more than one recipient.
  • They earn $100 through sales on your platform. For each customer payment, there's automatically a transfer to their Stripe balance. Fine, as expected.
  • Separately, they earn $2000 in affiliate referral fees. You owe them this money.
    • You want to manaully pay them $2000 as a one-time, ad-hoc payout using Stripe Connect.
    • Or, you want to automatically give them 5% of every sale of a seller they referred to the platform.
    • Ooops, you can't! 😧
If you want to use separate Charges and Transfers for international sellers, you'll have to have them sign up with a recipient service agreement.
  • You can only send cross-border payments if they've signed up with a recipient service agreement.
 

Case 2. Double currency conversion for international sellers, costing 2% in fees

👉
Situation: You, a US platform, plan to mainly pay an international seller affiliate fees. Accordingly, you set them up as an Express account with tos agreement recipient, to allow cross-border payments. When they later start selling, you lose money converting currencies back and forth.
  • ✅ When they refer somebody, you want to pay them $50 referral fee, and can easily do so as a cross-border Transfer. It's converted to their local currency. Nice one.
  • 😧 They start selling in their local currency, to customers in their country. Suddenly, doing business with you, a US platform, is not ideal. A recipient account can't have the card_payments capability, but that's cool, you say; simply use Separate Charges and Transfers. They price their product in EUR and expect to receive €XX after your platform fees. Unfortunately, because you're using Separate Charges and Transfers, the full amount of EUR is converted to USD first. Then the amount you specify then is converted back to EUR – resulting in a 2% fee overall.
 
Click to expand: Worked example (showing 2% loss)

Worked example, assuming we wanted to take a €20.30 fee on a €100 purchase:

  1. We create a Checkout Session with price €100.00. In the `payment_intent_data`, we specify that the recipient should receive €79.70. (Awesound will pay Stripe fees out of the remaining €20.30.)
  1. The full €100 is converted to USD. (Aside: Stripe fees are calculated on the Platform account. $3.80 = 2.9% of $120.53 + $0.30, in this case.)
    1. 👆 You can see confirmation here that we were planning to send €79.70 to the recipient. You can also see the currency conversion rate used: €1 = $1.2053.
  1. A transfer of €79.70 is initiated. But, this amount of EUR was converted to USD. So actually, a transfer amount of $96.06 was initiated. The rate 1.2053 was used.
    1. 👆 79.70 x 1.2053 = 96.06
  1. We attempted to pay the recipient 96.06, but this had to be converted back to EUR. They wind up receiving an amount we didn't specify: €78.11.
    1. €79.70 * 98% = €78.11.
  1. The total loss is 2%.
Expressed as a fraction of the Platform earnings, this is huge
The loss (€1.59 in that example) might look small in that (compared to €20.30 or €79.70). But many marketplace platforms charge a commission of around 5% (excl. processing fees).
  • A 2% fee would reduce the platform's take by 40%.
  • If you charge a 3.5% fee like Gumroad, then losing 2% is more than half your revenue.
  • For reference, Convertkit charges just 3.5% + $0.30 including credit card fees – that's just 0.6% above Stripe sticker-price fees. (If you want to price-match ConvertKit for your sellers, without a magic Stripe discount, you'd have to accept a loss of 1.4% per international transaction.)
If the platform doesn't eat the 2% fee, international sellers will not be happy that they're receiving less than the payout promised!
In this case, the seller was promised €79.70 but received €78.11. More coding would be required to compensate for the 2% loss. For example, promise €79.70 on the marketing page, but request a transfer of €79.70 / 0.98 in the API call.
 

Case 3: An international affiliate becomes a seller

(The opposite of Case 1.)
👉
Situation: You want to use on_behalf_of for a payment to a seller (to avoid currency conversion mess like the above), but you can't.
  • You originally let them sign up as service agreement type recipient. (Makes sense, if you thought they'd mainly be an affiliate, and need to receive ad-hoc / split payments.)
  • Now, you realise they've transitioned to mainly being a seller, not an affiliate, and you want to use on_behalf_of for purchases of their products: you'll have to ask them to start the Stripe Connect onboarding flow again, this time with service agreement type full.
https://stripe.com/docs/connect/destination-charges#settlement-merchant
  • full will allow you to add card_payments as a capability, which in turn will allow you to use the on_behalf_ofparameter.
 
 

Case 4: You want to support one-time-purchase and subscriptions for your international sellers.

 
transfer_data and on_behalf_of are not possible when creating a subscription via Stripe Checkout https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-subscription_data

My requirements

  • Each seller only needs to complete the Stripe onboarding once. All their earnings go to one Stripe Balance.
  • Account type must be "Custom" or "Express" [not "Standard"]. (Standard accounts can leave your platform at any time without notice, and the customer is on the connected account.)
  • Some sellers will be non-US, selling products in their home currency (e.g., EUR or GBP)
    • We must avoid the currency double-conversion problem (Case 2 above).
  • Each seller must be able to offer a mix of one-time-purchases (e.g., audiobooks) and subscriptions (e.g., paid podcast or multi-week course).
👉
You'd think setting up a subscription is just the same logically as setting up a bunch of payments… but apparently not. You cannot use on_behalf_of when creating a Subscription.

My solution

This is what I wound up doing, which seems messy, but isn't for no reason:
  • For single-purchase items: Prices are defined in the Platform account for single-purchase items (audiobooks).
    • Customers pay using a Destination Charge with on_behalf_of. Double currency conversion is avoided.
  • For subscriptions: Prices are defined on the Connected account for monthly subscriptions (paid podcasts)
    • Customers pay using a Direct Charge to the connected account.
    • stripeAccount
    • Double currency conversion is avoided. The platform listens for webhooks on connected accounts.
if (checkout_mode == "subscription" && connectStripeAcId) {
  // Customer is subscribing to a monthly paid podcast; use a Direct Charge to the platform account.
  // (Prices are defined on the connected account for recurring-subscription items like podcasts.)
  // connectStripeAcId will be present if publisher has completed Stripe Connect onboarding,
  //    and is ready to sell (has card_payments capability active)
  const stripeSession = await stripe.checkout.sessions.create(postData, {
    stripeAccount: connectStripeAcId,
  });
  res.json(stripeSession);
} else {
  // checkout_mode == "payment", use Platform account
  // (Prices are defined in the Platform account for single-purchase items like audiobooks.)
  const stripeSession = await stripe.checkout.sessions.create(postData);
  res.json(stripeSession);
}
If using Direct Charges to the connected account account like this, we also need to use the stripeAccount keyword when calling loadStripe
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY, {
    stripeAccount: connectStripeAcId, // e.g., "acct_1234ABCDFBHb3LtaI",
});
More notes to self on Stripe Subscriptions with international sellers on Stripe Connect.

Case 5: You want to move your sellers to your new Stripe account

Example scenario: You are a nascent UK Ltd. Company. Some sellers sign up as Connected Accounts under your Stripe UK account. Then you get funded by YC, you set up a Delaware C Corp. (👋 Stripe Atlas), move HQ to USA, and create a US Stripe account.
  • It's easy to contact Stripe support and ask them to copy over customers (individuals who made a purchase) to your new Stripe account.
  • It's NOT trivial to move the Connected Accounts over to your US Stripe account. It might even be impossible, I'm not sure. You want to be sure that the Platform account you're using when onboarding sellers is the main Stripe account you plan to use for years into future.